ctsim-7.0.0/0000755000175000017500000000000015253770310011652 5ustar kevinkevinctsim-7.0.0/include/0000755000175000017500000000000015253702131013271 5ustar kevinkevinctsim-7.0.0/include/backprojectors.h0000644000175000017500000001664415253543770016504 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: backproject.h ** Purpose: Backprojection classes ** Programmer: Kevin Rosenberg ** Date Started: June 2000 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef __BACKPROJECTORS_H #define __BACKPROJECTORS_H #include #include #include #include "imagefile.h" class Backproject; class ImageFile; class Projections; struct ReconstructionROI; class Backprojector { public: static const int BPROJ_INVALID; static const int BPROJ_TRIG; static const int BPROJ_TABLE; static const int BPROJ_DIFF; static const int BPROJ_IDIFF; static const int INTERP_INVALID; static const int INTERP_NEAREST; static const int INTERP_LINEAR; static const int INTERP_CUBIC; static const int INTERP_FREQ_PREINTERPOLATION; Backprojector (const Projections& proj, ImageFile& im, std::string_view backprojName, std::string_view interpName, const int interpFactor, const ReconstructionROI* pROI); ~Backprojector (); void BackprojectView (const double* const viewData, const double viewAngle); void PostProcessing(); bool fail() const {return m_fail;} const std::string& failMessage() const {return m_failMessage;} static int getBackprojectCount() {return s_iBackprojectCount;} static const char* const* getBackprojectNameArray() {return s_aszBackprojectName;} static const char* const* getBackprojectTitleArray() {return s_aszBackprojectTitle;} static int convertBackprojectNameToID (std::string_view name); static std::string_view convertBackprojectIDToName (const int bprojID); static const char* convertBackprojectIDToTitle (const int bprojID); static int getInterpCount() {return s_iInterpCount;} static const char* const * getInterpNameArray() {return s_aszInterpName;} static const char* const * getInterpTitleArray() {return s_aszInterpTitle;} static int convertInterpNameToID (std::string_view name); static std::string_view convertInterpIDToName (const int interpID); static const char* convertInterpIDToTitle (const int interpID); private: std::string m_nameBackproject; std::string m_nameInterpolation; int m_idBackproject; int m_idInterpolation; Backproject* m_pBackprojectImplem; bool m_fail; std::string m_failMessage; static const char* const s_aszBackprojectName[]; static const char* const s_aszBackprojectTitle[]; static const int s_iBackprojectCount; static const char* const s_aszInterpName[]; static const char* const s_aszInterpTitle[]; static const int s_iInterpCount; bool initBackprojector (const Projections& proj, ImageFile& im, std::string_view backprojName, std::string_view interpName, const int interpFactor, const ReconstructionROI* pROI); }; class Backproject { public: Backproject (const Projections& proj, ImageFile& im, int interpID, const int interpFactor, const ReconstructionROI* pROI); virtual ~Backproject (); virtual void BackprojectView (const double* const viewData, const double viewAngle) = 0; virtual void PostProcessing (); // call after backprojecting all views protected: void ScaleImageByRotIncrement (); void errorIndexOutsideDetector (int ix, int iy, double theta, double r, double phi, double L, int ni); void errorIndexOutsideDetector (int ix, int iy, double theta, double L, int ni); const Projections& proj; ImageFile& im; int interpType; ImageFileArray v; // reference to the image's pixel storage (bound in the constructor) kint32 nx; kint32 ny; double detInc; double rotScale; int iDetCenter; // index refering to L=0 projection int nDet; double xMin, xMax, yMin, yMax; // Retangular coords of phantom double xInc, yInc; // size of cells int m_interpFactor; double m_dFocalLength; double m_dSourceDetectorLength; bool m_bPostProcessingDone; private: Backproject (const Backproject& rhs); Backproject& operator= (const Backproject& rhs); }; class BackprojectTrig : public Backproject { public: BackprojectTrig (const Projections& proj, ImageFile& im, int interpID, const int interpFactor, const ReconstructionROI* pROI) : Backproject (proj, im, interpID, interpFactor, pROI) {} void BackprojectView (const double* const t, const double view_angle); }; class BackprojectTable : public Backproject { public: BackprojectTable (const Projections& proj, ImageFile& im, int interpID, const int interpFactor, const ReconstructionROI* pROI); virtual ~BackprojectTable (); virtual void BackprojectView (const double* const t, const double view_angle); virtual void PostProcessing (); // call after backprojecting all views protected: Array2d arrayR; Array2d arrayPhi; kfloat64** r; kfloat64** phi; }; class BackprojectDiff : public Backproject { public: BackprojectDiff (const Projections& proj, ImageFile& im, int interpID, const int interpFactor, const ReconstructionROI* pROI); ~BackprojectDiff (); virtual void BackprojectView (const double* const t, const double view_angle); virtual void PostProcessing (); // call after backprojecting all views protected: double start_r; double start_phi; double im_xinc, im_yinc; // The per-detector differences for linear interpolation, kept across // views: BackprojectView is called once per view -- two thousand times in // a large reconstruction -- and this was allocated and freed each time. std::vector m_vecDelta; }; class BackprojectIntDiff : public BackprojectDiff { public: BackprojectIntDiff (const Projections& proj, ImageFile& im, int interpID, const int interpFactor, const ReconstructionROI* pROI) : BackprojectDiff (proj, im, interpID, interpFactor, pROI) {} void BackprojectView (const double* const t, const double view_angle); }; class BackprojectEquilinear : public BackprojectTable { public: BackprojectEquilinear (const Projections& proj, ImageFile& im, int interpID, const int interpFactor, const ReconstructionROI* pROI) : BackprojectTable (proj, im, interpID, interpFactor, pROI) {} void BackprojectView (const double* const t, const double view_angle); virtual ~BackprojectEquilinear() {} }; class BackprojectEquiangular : public BackprojectTable { public: BackprojectEquiangular (const Projections& proj, ImageFile& im, int interpID, const int interpFactor, const ReconstructionROI* pROI) : BackprojectTable (proj, im, interpID, interpFactor, pROI) {} void BackprojectView (const double* const t, const double view_angle); virtual ~BackprojectEquiangular() {} }; #endif ctsim-7.0.0/include/timeformat.h0000644000175000017500000000263415253543770015632 0ustar kevinkevin/***************************************************************************** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef TIMEFORMAT_H #define TIMEFORMAT_H #include // A computation time, in seconds, as text. Chooses the unit so the number // sits between 1 and 1000 -- nanoseconds up to minutes -- and always gives // three decimals: 0.15 -> "150.000 ms", 2.5e-6 -> "2.500 us", 90 -> "1 min // 30.000 s". Zero or negative gives "0 s". Shared by the command-line // tools and the interface so a duration reads the same everywhere. std::string formatCalcTimeStd (double dSeconds); #endif ctsim-7.0.0/include/fourier.h0000644000175000017500000000743015253543770015135 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: fourier.h ** Purpose: Header for Fourier transform functions ** Programmer: Kevin Rosenberg ** Date Started: Dec 2000 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #include #include #ifdef HAVE_FFTW #include #endif class ImageFile; class Fourier { public: static void shuffleFourierToNaturalOrder (ImageFile& im); static void shuffleNaturalToFourierOrder (ImageFile& im); #ifdef HAVE_FFTW static void shuffleFourierToNaturalOrder (fftw_complex* pc, const int n); static void shuffleNaturalToFourierOrder (fftw_complex* pc, const int n); #endif // Odd Number of Points // Natural Frequency Order: -(n-1)/2...-1,0,1...(n-1)/2 // Fourier Frequency Order: 0, 1..(n-1)/2,-(n-1)/2...-1 // Even Number of Points // Natural Frequency Order: -n/2...-1,0,1...((n/2)-1) // Fourier Frequency Order: 0,1...((n/2)-1),-n/2...-1 template static void shuffleNaturalToFourierOrder (T* pVector, const int n) { std::vector vecTemp (n); T* pTemp = vecTemp.data(); int i; if (isOdd(n)) { // Odd int iHalfN = (n - 1) / 2; pTemp[0] = pVector[iHalfN]; for (i = 0; i < iHalfN; i++) pTemp[i + 1] = pVector[i + 1 + iHalfN]; for (i = 0; i < iHalfN; i++) pTemp[i + iHalfN + 1] = pVector[i]; } else { // Even int iHalfN = n / 2; pTemp[0] = pVector[iHalfN]; for (i = 0; i < iHalfN - 1; i++) pTemp[i + 1] = pVector[i + iHalfN + 1]; for (i = 0; i < iHalfN; i++) pTemp[i + iHalfN] = pVector[i]; } for (i = 0; i < n; i++) pVector[i] = pTemp[i]; } template static void shuffleFourierToNaturalOrder (T* pVector, const int n) { std::vector vecTemp (n); T* pTemp = vecTemp.data(); int i; if (isOdd(n)) { // Odd int iHalfN = (n - 1) / 2; pTemp[iHalfN] = pVector[0]; for (i = 0; i < iHalfN; i++) pTemp[i + 1 + iHalfN] = pVector[i + 1]; for (i = 0; i < iHalfN; i++) pTemp[i] = pVector[i + iHalfN + 1]; } else { // Even int iHalfN = n / 2; pTemp[iHalfN] = pVector[0]; for (i = 0; i < iHalfN; i++) pTemp[i] = pVector[i + iHalfN]; for (i = 0; i < iHalfN - 1; i++) pTemp[i + iHalfN + 1] = pVector[i+1]; } for (i = 0; i < n; i++) pVector[i] = pTemp[i]; } #if 0 static void shuffleNaturalToFourierOrder (float* pdVector, const int n); static void shuffleNaturalToFourierOrder (double* pdVector, const int n); static void shuffleNaturalToFourierOrder (std::complex* pdVector, const int n); static void shuffleFourierToNaturalOrder (float* pdVector, const int n); static void shuffleFourierToNaturalOrder (double* pdVector, const int n); static void shuffleFourierToNaturalOrder (std::complex* pdVector, const int n); #endif }; ctsim-7.0.0/include/dicom.h0000644000175000017500000000601515253543770014553 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: dicom.h ** Purpose: DICOM import and export, through DCMTK ** Programmer: Kevin Rosenberg ** Date Started: March 2001 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef CTSIM_DICOM_H #define CTSIM_DICOM_H #include #include #if HAVE_DCMTK #include #include #include #ifdef GCCSUNOS #include #endif #include "ctsupport.h" // DCMTK replaces the CTN library as of 6.3.0. #include "dcmtk/config/osconfig.h" #include "dcmtk/dcmdata/dctk.h" #include class ImageFile; class Projections; class DicomImporter { private: std::string m_strFilename; bool m_bFail; std::string m_strFailMessage; int m_iContents; ImageFile* m_pImageFile; Projections* m_pProjections; DcmFileFormat* m_pFile; void loadImage(unsigned short iNRows, unsigned short iNCols, unsigned short iBitsAllocated, unsigned short iBitsStored, unsigned short iHighBit, unsigned short iPixRep); void loadProjections(); enum { TAG_GROUP_SOMATOM = 0x7fe1, TAG_MEMBER_SOMATOM_DATA = 0x1000, }; public: enum { DICOM_CONTENTS_INVALID = -1, DICOM_CONTENTS_IMAGE, DICOM_CONTENTS_PROJECTIONS, }; DicomImporter (std::string_view svFile); ~DicomImporter(); bool testImage() const {return m_iContents == DICOM_CONTENTS_IMAGE;} bool testProjections() const {return m_iContents == DICOM_CONTENTS_PROJECTIONS;} bool fail() const {return m_bFail;} const std::string& failMessage() const {return m_strFailMessage;} ImageFile* getImageFile() const {return m_pImageFile;} Projections* getProjections() const {return m_pProjections;} }; class DicomExporter { private: const ImageFile* m_pImageFile; std::string m_strFilename; bool m_bFail; std::string m_strFailMessage; DcmFileFormat* m_pObject; bool createDicomObject(); public: DicomExporter (ImageFile* pImageFile); ~DicomExporter(); bool writeFile (std::string_view svFilename); bool fail() const {return m_bFail;} const std::string& failMessage() const {return m_strFailMessage;} }; #endif // HAVE_DCMTK #endif // CTSIM_DICOM_H ctsim-7.0.0/include/phantom.h0000644000175000017500000002621615253666241015132 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: phantom.h ** Purpose: Header file for Phantom objects ** Programmer: Kevin Rosenberg ** Date Started: July 1, 1984 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef PHANTOM_H #define PHANTOM_H #include #include #include #include #include "ctsupport.h" enum PhmElemType { PELEM_INVALID, PELEM_RECTANGLE, PELEM_TRIANGLE, PELEM_ELLIPSE, PELEM_SECTOR, PELEM_SEGMENT, PELEM_RASTER // pixels from a PNG image }; /* Codes for Coordinate Types */ /* Defines coords for isPointInside() */ enum CoordType { PELEM_COORD, /* Normalized PElem Coordinates */ PHM_COORD /* World phantom Coordinates */ }; class PhantomElement { public: PhantomElement (std::string_view type, const double cx, const double cy, const double u, const double v, const double rot, const double atten); // raster element. The PNG's grayscale range [0,1] is mapped to // [attenMin, attenMax]; (u,v) are half-extents like every other element, // so the image spans cx +/- u, cy +/- v before rotation by rot degrees. PhantomElement (std::string_view pngFilename, const double cx, const double cy, const double u, const double v, const double rot, const double attenMin, const double attenMax); bool isRaster() const { return m_type == PELEM_RASTER; } bool fail() const { return m_bFail; } const std::string& failMessage() const { return m_strFailMessage; } // Attenuation contributed at world point (x,y): 0 outside the element. double attenuationAt (double x, double y) const; // Exact integral of attenuation along the world segment (x1,y1)-(x2,y2). double lineIntegral (double x1, double y1, double x2, double y2) const; ~PhantomElement (); bool isPointInside (double x, double y, const CoordType coord_type) const; bool clipLineNormalizedCoords (double& x1, double& y1, double& x2, double& y2) const; bool clipLineWorldCoords (double& x1, double& y1, double& x2, double& y2) const; int nOutlinePoints() const {return m_nPoints;} double* rectLimits() {return m_rectLimits;} const double* xOutline() {return m_vXOutline.data();} const double* yOutline() {return m_vYOutline.data();} const double* xOutline () const {return m_vXOutline.data();} const double* yOutline () const {return m_vYOutline.data();} double atten() const {return m_atten;} const std::string& rasterFilename() const {return m_strRasterFile;} // The raster pixels as attenuation, column-major with y upward, for // drawing the element rather than only its outline. void setRasterFilename (std::string_view str) { m_strRasterFile = str; } int rasterWidth() const {return m_nRasterX;} int rasterHeight() const {return m_nRasterY;} double rasterAtten (int ix, int iy) const { return m_vRasterAtten[static_cast(ix) * m_nRasterY + iy]; } double rasterAttenMin() const {return m_dRasterAttenMin;} double rasterAttenMax() const {return m_dRasterAttenMax;} double xmin() const {return m_xmin;} double xmax() const {return m_xmax;} double ymin() const {return m_ymin;} double ymax() const {return m_ymax;} double rot() const {return m_rot;} double cx() const {return m_cx;} double cy() const {return m_cy;} double u() const {return m_u;} double v() const {return m_v;} static PhmElemType convertNameToType (std::string_view name); void printDefinition (std::ostream& os) const; void printDefinition (std::ostringstream& os) const; private: PhmElemType m_type; // pelem type (box, ellipse, etc) double m_cx, m_cy; // center of pelem double m_u, m_v; // size of pelem double m_atten; // 6.1.0 raster element state (empty for geometric elements) std::vector m_vRasterAtten; // column-major, m_vRasterAtten[ix * m_nRasterY + iy], y increasing upward int m_nRasterX = 0; int m_nRasterY = 0; double m_dRasterAttenMin = 0.; double m_dRasterAttenMax = 0.; std::string m_strRasterFile; bool m_bFail = false; std::string m_strFailMessage; void initCommon (); bool loadRasterPNG (std::string_view filename); // X-ray attenuation coefficient double m_rot; // pelem rotation angle (in radians) double *m_x, *m_y; // ptr to array of points in obj world coord int m_nPoints; // number of points in outline arrays double m_xmin, m_xmax, m_ymin, m_ymax; // pelem limits GRFMTX_2D m_xformPhmToObj; // map from phantom to normalized pelem coords GRFMTX_2D m_xformObjToPhm; // map from normalized pelem coords to phantom coords std::vector m_vXOutline; std::vector m_vYOutline; double m_rectLimits[4]; static const int POINTS_PER_CIRCLE; static const double SCALE_PELEM_EXTENT; // increase pelem limits by 0.5% static const char* convertTypeToName (PhmElemType iType); void makeTransformMatrices (); void makeVectorOutline (); void calcArcPoints (double x[], double y[], const int pts, const double xcent, const double ycent, const double r, const double start, const double stop); void calcEllipsePoints (double x[], double y[], const int pts, const double u, const double v); static int numCirclePoints (double theta); PhantomElement (const PhantomElement& rhs); // copy constructor PhantomElement& operator= (const PhantomElement&); // assignment operator }; enum PhantomComposition { P_PELEMS, // Phantom made of PElems P_UNIT_PULSE, // Special phantom, not made of pelems P_FILTER // defined only by this type }; ////////////////////////////////////////////////////// // Phantom Class Declaration ////////////////////////////////////////////////////// class SGP; class ImageFile; class Phantom { public: static const int PHM_INVALID; static const int PHM_HERMAN; static const int PHM_SHEPP_LOGAN; static const int PHM_UNITPULSE; Phantom (); Phantom (std::string_view phmName); ~Phantom (); void setComposition (PhantomComposition composition) { m_composition = composition; } PhantomComposition getComposition () const { return m_composition; } bool createFromPhantom (std::string_view phmName); bool createFromPhantom (const int phmid); bool createFromFile (std::string_view fname); bool fileWrite (std::string_view fname); void addPElem (const PhantomElement& pelem); void addPElem (std::string_view composition, const double cx, const double cy, const double u, const double v, const double rot, const double atten); // add a raster (PNG) element; returns false and sets fail() on error. // A phantom that is one PNG image: centred on the origin, one world unit // per pixel, black at 0 and full-scale white at attenuation 1. Colour // images are taken as their luminance. The name recorded in the // definition (and so written to a .phm file) is pszReferenceName if // given, else the path -- a tool writing a .phm beside the image records // just the file name, so the pair can be moved together. bool createFromPNG (std::string_view pngFilename, std::string_view svReferenceName = {}); bool addRasterPElem (std::string_view pngFilename, const double cx, const double cy, const double u, const double v, const double rot, const double attenMin, const double attenMax); void convertToImagefile (ImageFile& im, double dViewRatio, const int in_nsample, const int trace) const; void convertToImagefile (ImageFile& im, double dViewRatio, const int in_nsample, const int trace, const int colStart, const int colCount, bool bStoreAtColumnPos) const; void convertToImagefile (ImageFile& im, int iNX, double dViewRatio, const int in_nsample, const int trace, const int colStart, const int colCount, int iStorageOffset) const; void printDefinitions (std::ostream& os) const; void printDefinitions (std::ostringstream& os) const; bool fail() const {return m_fail;} const std::string& failMessage() const {return m_failMessage;} const std::string& name() const {return m_name;} int id() const {return m_id;} #ifdef HAVE_SGP void show () const; void show (SGP& sgp) const; void draw (SGP& sgp) const; #endif void addStdHerman (); void addStdSheppLogan (); void print (std::ostream& os) const; void print (std::ostringstream& os) const; double maxAxisLength () const { return maxValue (m_xmax - m_xmin, m_ymax - m_ymin); } double getDiameterBoundaryCircle() const { return SQRT2 * maxAxisLength(); } double xmin() const {return m_xmin;} double xmax() const {return m_xmax;} double ymin() const {return m_ymin;} double ymax() const {return m_ymax;} std::list& listPElem() {return m_listPElem;} const std::list& listPElem() const {return m_listPElem;} int nPElem() const {return m_nPElem;} static int getPhantomCount() {return s_iPhantomCount;} static const char** getPhantomNameArray() {return s_aszPhantomName;} static const char** getPhantomTitleArray() {return s_aszPhantomTitle;} static int convertNameToPhantomID (std::string_view phmName); static const char* convertPhantomIDToName (const int phmID); static const char* convertPhantomIDToTitle (const int phmID); private: PhantomComposition m_composition; int m_nPElem; // number of pelems in phantom double m_xmin, m_xmax, m_ymin, m_ymax; // extent of pelems in pelem coordinates mutable std::list m_listPElem; // pelem lists std::string m_name; int m_id; bool m_fail; std::string m_failMessage; static const char* s_aszPhantomName[]; static const char* s_aszPhantomTitle[]; static const int s_iPhantomCount; void init(); Phantom (const Phantom& rhs); // copy constructor Phantom& operator= (const Phantom&); // assignment operator }; using PElemIterator = std::list::iterator; using PElemConstIterator = std::list::const_iterator; #endif ctsim-7.0.0/include/transformmatrix.h0000644000175000017500000000364015253543770016721 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: sgp.h ** Purpose: Header file for Simple Graphics Package ** Author: Kevin Rosenberg ** Date Started: 1984 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef __H_TRANSFORMMATRIX #define __H_TRANSFORMMATRIX #include class TransformationMatrix2D { public: double mtx[3][3]; TransformationMatrix2D () {}; TransformationMatrix2D (double m[3][3]); void setIdentity(); void setTranslate (double x, double y); void setScale (double sx, double sy); void setShear (double shrx, double shry); void setRotate (double theta); double determinant () const; const TransformationMatrix2D invert () const; void transformPoint (double *pX, double *pY) const; void print (std::ostream& ostr) const; friend const TransformationMatrix2D operator* (const TransformationMatrix2D& lhs, const TransformationMatrix2D& rhs); }; const TransformationMatrix2D operator* (const TransformationMatrix2D& lhs, const TransformationMatrix2D& rhs); #endif ctsim-7.0.0/include/plotfile.h0000644000175000017500000001447215253543770015304 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: plotfile.h ** Purpose: PlotFile class header ** Programmer: Kevin Rosenberg ** Date Started: Dec 2000 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef PLOTFILE_H #define PLOTFILE_H #include #include #include #include #include #include #include #include #include #include #include "array2dfile.h" #include "ctsupport.h" #include "plotfile.h" // Plotfile structure: // 1. Lines that begin with # are comments // 2. ASCII file format // 3. Header lines begin with and end with // 4. Valid headers // (signifies beginning of plotfile) // (beginning of description lines) // (signifies beginning of ezset commands) // Beginning of data columns // 5. Data is ASCII file format, one record per line // Number of columns is variable and is set by ncolumns header class PlotFile { private: std::string m_strFilename; std::string m_strDate; std::vector m_vecStrDescriptions; std::vector m_vecStrEzsetCommands; std::vector m_vecCurves; int m_iNumColumns; int m_iNumRecords; bool m_bScatterPlot; // Optional display range for the y axis: the levels the plot was being // shown at when it was saved, so it opens the same way. Only the HDF5 // format stores these; the text format has no field for them. bool m_bHasYRange; double m_dYMin; double m_dYMax; // History, as images and projections carry it: the operations that produced // this plot and, before them, the history of the data they were made from. // HDF5 only. std::vector m_vecLabels; bool writeHDF5 (std::string_view filename); bool readHDF5 (std::string_view filename); bool headerRead (std::iostream& os); bool headerWrite (std::iostream& os); bool columnsRead (std::iostream& os); bool columnsWrite (std::iostream& os); void initHeaders (); PlotFile (const PlotFile& rhs); // copy constructor PlotFile& operator= (const PlotFile&); // assignment operator public: PlotFile (int iNColumns, int iNRecords); PlotFile (void); ~PlotFile (); void setCurveSize (int iNCurves, int iNRecords, bool bScatterPlot = false); void addDescription (std::string_view desc) { m_vecStrDescriptions.emplace_back (desc); } void addEzsetCommand (std::string_view cmd) { m_vecStrEzsetCommands.emplace_back (cmd); } // A span rather than a bare pointer: the column has to hold at least // nRecords values, and with a pointer there was no way to say so or to // check it. A std::vector converts on its own; a raw pointer that is // still the only thing available says its length: {p, n}. bool addColumn (int iCol, std::span column); bool addColumn (int iCol, std::span column); void getColumn (int iCol, double *pdColumnData) const; const std::string& getDate () const { return m_strDate; } int getNumColumns () const { return m_iNumColumns; } int getNumRecords () const { return m_iNumRecords; } bool getIsScatterPlot() const { return m_bScatterPlot; } bool getMinMax (int startingCol, double& min, double& max) const; bool statistics (int startingCol, double& min, double& max, double& mean, double& mode, double& median, double &stddev) const; unsigned int getNumDescriptions (void) const { return m_vecStrDescriptions.size(); } const std::string& getDescription (int iDescIndex) const { return m_vecStrDescriptions[iDescIndex]; } unsigned int getNumEzsetCommands (void) const { return m_vecStrEzsetCommands.size(); } const std::string& getEzsetCommand (int iIndex) const { return m_vecStrEzsetCommands[iIndex]; } // Reads either format, told apart by content. bool fileRead (std::string_view filename); // Writes in the default format (HDF5 unless ctsimSetDefaultFileFormat says // otherwise); the legacy text format is always available via fileWriteText. bool fileWrite (std::string_view filename); bool fileWriteText (std::string_view filename); // Y-axis display range. bool hasYRange () const { return m_bHasYRange; } double yMin () const { return m_dYMin; } double yMax () const { return m_dYMax; } void setYRange (double dMin, double dMax) { m_bHasYRange = true; m_dYMin = dMin; m_dYMax = dMax; } void clearYRange () { m_bHasYRange = false; } // History. void addHistory (std::string_view operation, const HistoryParams& params, double dCalcTime = 0.); void labelAdd (const Array2dFileLabel& label) { m_vecLabels.push_back (label); } // Appends every label of an image or projection set, so a plot made from // it carries the same past. void labelsCopyFrom (const Array2dFile& rFile); void labelsCopyFrom (const Array2dFileLabel& rLabel); unsigned int nLabels () const { return m_vecLabels.size(); } const Array2dFileLabel& labelGet (unsigned int i) const { return m_vecLabels[i]; } const std::vector& labels () const { return m_vecLabels; } std::vector& labels () { return m_vecLabels; } const std::string& getFilename (void) const { return m_strFilename; } void printHeaders (std::ostream& os) const; void printHeaders (std::ostringstream& os) const; void printHeadersBrief (std::ostream& os) const; void printHeadersBrief (std::ostringstream& os) const; }; #endif ctsim-7.0.0/include/ct.h0000644000175000017500000000537315253543770014074 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: ct.h ** Purpose: Master header file for CTSim ** Programmer: Kevin Rosenberg ** Date Started: Aug 1984 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef CT_H #define CT_H #define NO_MAIN 1 // filter out all old main blocks #if defined(__cplusplus) && __cplusplus < 202002L #error "CTSim 6.1.0 and later require a C++20 compiler (use -std=c++20)" #endif #ifdef HAVE_CONFIG_H #include #endif #ifdef _DEBUG #undef DEBUG #define DEBUG 1 #endif // The standard library. These were once behind HAVE_ANSI_CPP, with a // second set of -style includes for a compiler that had no C++ // library; CTSim has required C++20 since 6.1.0, so that branch was dead // and the probes that chose between them answered a question nobody asks. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_PNG #include "png.h" #endif #if defined(HAVE_GETOPT_H) || defined(HAVE_GETOPT_LONG) #include #endif #ifdef HAVE_FFTW #include #endif #include "ctsupport.h" #include "fnetorderstream.h" #ifdef HAVE_SGP #include "ezplot.h" #include "sgp.h" #endif #include "array2d.h" #include "array2dfile.h" #include "fnetorderstream.h" #include "imagefile.h" #include "phantom.h" #include "scanner.h" #include "backprojectors.h" #include "filter.h" #include "fourier.h" #include "procsignal.h" #include "projections.h" #include "reconstruct.h" #include "plotfile.h" #include "trace.h" #include "ctglobals.h" // Macro to remove compiler warnings about unused variables #define UNUSED(expr) do { (void)(expr); } while (0) #endif ctsim-7.0.0/include/ctshell.h0000644000175000017500000000267615253543770015127 0ustar kevinkevin/***************************************************************************** ** ctshell.h ** ** The command-line functions -- phm2if, pjrec and the rest -- and the shell ** that dispatches them. One implementation, used by every binary that ** offers them: ctsim, ctsim-nogui, and each function alias. *****************************************************************************/ #ifndef CTSHELL_H #define CTSHELL_H #include // True where the string names one of the functions the shell offers. A // caller asks this before doing anything expensive: ctsim consults it to // decide whether it was asked for a function or for its interface, and must // decide before constructing a QApplication so that a text invocation works // on a machine with no display. bool ctsimIsFunctionName (const char* pszName); // Run the shell: dispatch one function named by argv, or read functions // from a prompt where none was named. Returns the exit code. int ctsimShellMain (int argc, char* argv[]); // Run one command line and return everything it printed. For a caller // that offers the shell without a terminal -- the graphical shell window -- // so that what it does and what the prompt does cannot drift apart. int ctsimShellCommand (const char* pszLine, std::string& strOutput); // The same, wrapped in the FFTW wisdom cache and a catch-all for // exceptions. This is what a binary's main() calls. int ctsimShellRun (int argc, char* argv[]); #endif ctsim-7.0.0/include/pol.h0000644000175000017500000001576515253543770014266 0ustar kevinkevin/***************************************************************************** ** Name: pol.h ** Purpose: Header file for Problem Oriented Language Class ** Programmer: Kevin Rosenberg ** Date Started: 1984 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef __H_POL #define __H_POL #include #include "hashtable.h" #include #include #include class POL { public: // codes for pol_usefile enum { P_USE_STR = 1, // use string as input source P_USE_FILE, // use file as input source }; POL(); ~POL(); void init (); void addSkipWord (std::string_view w); void addSkipChar (int c); void addKeyword (std::string_view str, int code); bool readWord (std::string_view search, int nlet); bool readUserToken (std::string& rString, int* code); bool readString (std::string& rString); bool readInteger (int *n, int typecode, bool boundcode, int bb1, int bb2); bool readFloat (double *n, double typecode, bool boundcode, double bb1, double bb2); bool skipTokens (); void reader (); bool readText (char *str, int lim); // lim is the size of the buffer bool readText (std::string& rString); // the whole of it, however long void usefile (int source, std::string_view fn); void closefile (); int lookchar (); int inchar (); void ungetch (int c); int get_inputline (FILE *fp); void set_inputline (std::string_view line); enum { MAXIDENT = 20, MAXSKIPWORD = 20, MAXSKIPCHAR = 20, }; // token types enum { TT_STRING = 1, // string token TT_INT, // integer token TT_REAL, // floating point token TT_ALPHA, // alphabetic token TT_ALPNUM, // alphanumeric token TT_NUMALPHA, TT_SPECLCHAR, TT_EOF, // end of file reached TT_ERROR, // error in token, caused by call to wrong type of token reader TT_BLANK, // white space token. pol_tok() skips these TT_USERTOK, // user defined token }; private: // codes for pol_int and pol_float // if in reject catagory, get new number from terminal enum { P_FLTINT = 1, // get a real or integer number P_BFLTINT, // get a real or integer number, clip against bounds P_CBFLTINT, // get real or int, reject if outside bounds P_FLT, // get a real number P_BFLT, // get a real, clip against bounds P_CBFLT, // get a floating, reject if outside bounds P_INT, // get a integer number P_BINT, // get a integer, clip against bounds P_CBINT, // get a integer, reject if outside bounds }; #define LETTER 'a' #define DIGIT '0' // typedef std::map KeywordCodeList; public: struct token_st { int ready; // TRUE if token is ready // The token's text. It was a fixed 201-character array, and a longer // token was reported as an error and truncated. std::string tokstr; int type; // type of token 'TT_' int code; // holds code for user defined tokens double fnum; // real value of token int inum; // integer value of token }; using TOKEN = struct token_st; struct token_st token; // current token // Tables words stored with install() & found with lookup() KeywordCodeHashTable skiptable; // words to ignore and skip KeywordCodeHashTable cmdtable; // pol parameter commands KeywordCodeHashTable usertable; // user defined symbols struct metachar { char eoc; /* end of command character */ char str; /* string delimiter */ char com; /* comment character */ char cmd; /* pol parameter command character */ char prg; /* program load character */ char con; /* continuation across newline character */ char out; /* character that delimits output to terminal */ char ter; /* character indicates insertion of input from terminal */ char inb; /* input from graphics device */ } meta; std::string m_strSkipChars; // characters to skip bool m_bTrace; bool m_bNewlineIsEOC; struct KeywordCodeList { const char *keyword; int code; }; static const struct KeywordCodeList cmdlist[]; static const unsigned int NUMCMD; // Internal codes for pol commands enum { PC_EOC = 1, PC_STR, PC_COM, PC_CMD, PC_PRG, PC_CON, PC_OUT, PC_TER, PC_INB, PC_NL_EOC, PC_NL_NEOC, PC_TRON, PC_TROFF, PC_FILE, PC_DUMP, }; enum { INPUT_STREAM = 1, INPUT_FILE, INPUT_STRING, }; // The stack of inputs being read, innermost last. It was two fixed // arrays of eight with an index into them; the name array was filled with // strdup and never read or freed. struct InputSource { FILE* fp = nullptr; // nullptr when the input is a string std::string strName; // the file it came from, for messages }; std::vector m_vecSources; // The line being read, and how far into it the reader has got. It was a // fixed 1024-character array: a longer line was silently truncated, and // the limit was arbitrary. std::string inputline; std::size_t lineptr = 0; std::stack m_stackPushBackInput; bool skipSingleToken (char term[]); int tok (struct token_st *token); void dumptok (struct token_st *token); int getpol_tok (struct token_st *token); int getcmd (); int gettok (TOKEN *tok); // Each fills the string it is given, growing it as far as the input // needs; none of them has a limit to be passed or exceeded. void getblank (std::string& s); int getalpha (std::string& s); void getquote (std::string& qs); void getescape (std::string& s, int delim); int getnumber (std::string& str, double* fnum, int* inum); void eatline (); int type (int c); int getch (FILE *fp); }; #endif ctsim-7.0.0/include/sgp.h0000644000175000017500000002465615253666241014263 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: sgp.h ** Purpose: Header file for Simple Graphics Package ** Author: Kevin Rosenberg ** Date Started: 1984 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef __H_SGP #define __H_SGP #if HAVE_CONFIG_H #include "config.h" #endif #include "transformmatrix.h" #include #include #ifdef HAVE_QT // Forward declarations only: sgp.h is included by libctsim, which must not // need the Qt include path. The Qt types live behind pointers. class QPainter; class QPen; class QFont; #endif class SGPDriver { private: int m_iPhysicalXSize; int m_iPhysicalYSize; std::string m_sWindowTitle; int m_idDriver; #ifdef HAVE_QT QPainter* m_pPainter; #endif int m_idG2; public: enum { SGPDRIVER_WXWINDOWS = 1, SGPDRIVER_G2 = 2, SGPDRIVER_OPENGL = 4, SGPDRIVER_QT = 8, }; #ifdef HAVE_QT // The Qt backend. The painter must outlive the driver; the caller // owns it (a widget's paintEvent, a QImage, a QPrinter). SGPDriver (QPainter* pPainter, int xsize, int ysize); #endif SGPDriver (std::string_view svWinTitle = "", int xsize = 640, int ysize = 480); ~SGPDriver (); int getPhysicalXSize () const { return m_iPhysicalXSize; } int getPhysicalYSize () const { return m_iPhysicalYSize; } const std::string& getWindowTitle () const { return m_sWindowTitle; } bool isWX () const { return (m_idDriver & SGPDRIVER_WXWINDOWS ? true : false); } bool isG2 () const { return (m_idDriver & SGPDRIVER_G2 ? true : false); } bool isQt () const { return (m_idDriver & SGPDRIVER_QT ? true : false); } int idG2 () const { return m_idG2; } #ifdef HAVE_QT QPainter* idQt () const { return m_pPainter; } void setPainter (QPainter* p) { m_pPainter = p; } #endif }; class SGP_RGBColor; class SGP { private: int m_iPhysicalXSize; // Physical Window size int m_iPhysicalYSize; SGPDriver m_driver; double xw_min; // Window extents double yw_min; double xw_max; double yw_max; double xv_min; // Viewport extents double yv_min; double xv_max; double yv_max; double viewNDC[4]; // Viewport array for clip_rect() int m_iCurrentPhysicalX; int m_iCurrentPhysicalY; double m_dCurrentWorldX; double m_dCurrentWorldY; double m_dTextAngle; int m_iTextPointSize; bool m_bRecalcTransform; double m_dPointsPerPixel; // points (72pt/in) per screen pixel; int m_iLinestyle; int m_iMarker; // Master coordinates are coordinates before CTM transformation // World coordinates are coordinates defined by setWindow() // Normalized device coordinates range from 0. to 1. in both axes TransformationMatrix2D wc_to_ndc; // World coord to NDC matrix TransformationMatrix2D mc_to_ndc; // Master to NDC TransformationMatrix2D ndc_to_mc; // NDC to Master TransformationMatrix2D m_ctm; // Current transfromation matrix void calc_transform (); #if HAVE_QT QPen* m_pQtPen; QFont* m_pQtFont; void initFromPainter (QPainter* pPainter); void qtRelease (); // The Qt side of each drawing primitive, implemented in sgp_qt.cpp so the // Qt headers are confined to that one file. void qtLine (int x1, int y1, int x2, int y2); void qtPoint (int x, int y); void qtEraseWindow (); void qtSetTextColor (int iFG, int iBG); void qtSetColor (int icol); void qtSetPenWidth (int iWidth); void qtSetRasterOp (int ro); void qtSetLineStyle (int style); void qtDrawText (std::string_view message); void qtSetFontPointSize (double dPoints); void qtGetTextExtent (std::string_view text, int* piW, int* piH); int qtCharHeight (); int qtCharWidth (); void qtSetTextBold (bool bBold); int qtTextWidth (const char* pszText); #endif public: // The colour table is plain data; the Qt backend maps it to QColor. static SGP_RGBColor s_aRGBColor[]; static int s_iRGBColorCount; private: public: enum { // linestyles LS_NOLINE = 0, LS_SOLID = 0xffff, LS_DASH1 = 0xff00, LS_DASH2 = 0xf0f0, LS_DASH3 = 0xcccc, LS_DASH4 = 0xff3e, LS_DOTTED = 0xaaaa, }; enum { // Codes for marker symbols MARKER_POINT = 0, // small dot MARKER_SQUARE = 1, // empty square MARKER_FSQUARE = 2, // filled square MARKER_DIAMOND = 3, // empty diamond MARKER_FDIAMOND = 4, // filled diamond MARKER_CROSS = 5, // cross MARKER_XCROSS = 6, // x MARKER_CIRCLE = 7, // open circle MARKER_FCIRCLE = 8, // filled circle MARKER_BSQUARE = 9, // big open square MARKER_BDIAMOND = 10, // big open diamond }; enum { MARK_COUNT = 11, }; static const unsigned char MARKER_BITMAP[MARK_COUNT][5]; SGP (const SGPDriver& driver); ~SGP(); void drawCircle (const double r); void drawArc (const double r, double start, double stop); void drawRect (double xmin, double ymin, double xmax, double ymax); void lineAbs(double x, double y); void moveAbs(double x, double y); void lineRel(double x, double y); void moveRel(double x, double y); void drawText (std::string_view message); void polylineAbs(double x[], double y[], int n); void markerAbs (double x, double y); void markerRel(double x, double y); void pointAbs(double x, double y); void pointRel(double x, double y); void eraseWindow (); void setWindow (double xmin, double ymin, double xmax, double ymax); void setViewport (double xmin, double ymin, double xmax, double ymax); void frameViewport(); void setColor (int icol); void setLineStyle (int style); void setTextSize (double height); void setTextNDCSize (double height); void setTextPointSize (double height); // Bold, for text that has to be read at a small size: a legend entry // names a curve and is usually the smallest text on a plot. void setTextBold (bool bBold); // The width this string actually occupies, in world coordinates. An // average character width is not enough to size a box around text: a // proportional font makes "reconstruct" much wider than eleven averages. double getTextWidth (const char* pszText); // A width in device pixels as a width in world coordinates. Margins that // should look the same whatever the window is are stated in pixels and // converted here; getTextWidth is this applied to a measured string. double deviceToWorldWidth (double dPixels); void setTextAngle (double angle); void setTextColor (int iFGcolor, int iBGcolor); void setPenWidth (int width); void setMarker (int idMarker); void setRasterOp (int ro); void getWindow (double& xmin, double& ymin, double& xmax, double& ymax); void getViewport (double& xmin, double& ymin, double& xmax, double& ymax); void getTextExtent (std::string_view text, double* x, double* y); double getCharHeight (); double getCharWidth (); SGPDriver& getDriver() {return m_driver;} const SGPDriver& getDriver() const {return m_driver;} void ctmClear (); void ctmSet (const TransformationMatrix2D& m); void preTranslate (double x, double y); void postTranslate (double x, double y); void preScale (double sx, double sy); void postScale (double sx, double sy); void preRotate (double theta); void postRotate (double theta); void preShear (double shrx, double shry); void postShear (double shrx, double shry); void transformNDCtoMC (double* x, double* y); void transformMCtoNDC (double* x, double* y); void transformMCtoNDC (double xIn, double yIn, double* xOut, double* yOut); void stylusNDC (double x, double y, bool beam); void pointNDC (double x, double y); void markerNDC (double x, double y); #if HAVE_QT void setPainter (QPainter* pPainter); #endif }; enum { C_BLACK = 0, // color codes C_BLUE = 1, C_GREEN = 2, C_CYAN = 3, C_RED = 4, C_MAGENTA = 5, C_BROWN = 6, C_GRAY = 7, C_LTGRAY = 8, C_LTBLUE = 9, C_LTGREEN = 10, C_LTCYAN = 11, C_LTRED = 12, C_LTMAGENTA = 13, C_YELLOW = 14, C_WHITE = 15, // Curve colours, for plots with several lines on white. The sixteen // above are the old fixed palette and are used for titles, axes and // labels, so they are left as they are; these are added rather than // redefined. They are the Okabe-Ito qualitative set, which was chosen // for its separation under the common forms of colour blindness, less // its yellow, which is illegible on white. Every one is dark enough to // read as a thin line on a white page. C_CURVE_1 = 16, // blue C_CURVE_2 = 17, // vermillion C_CURVE_3 = 18, // bluish green C_CURVE_4 = 19, // reddish purple C_CURVE_5 = 20, // orange C_CURVE_6 = 21, // dark magenta C_CURVE_7 = 22, // black C_CURVE_8 = 23, // olive }; // The curve colours in order, for a caller assigning one per curve. constexpr int kCurveColorCount = 8; constexpr int kCurveColor[kCurveColorCount] = { C_CURVE_1, C_CURVE_2, C_CURVE_3, C_CURVE_4, C_CURVE_5, C_CURVE_6, C_CURVE_7, C_CURVE_8 }; enum RasterOp { RO_AND = 1, RO_AND_INVERT, RO_AND_REVERSE, RO_CLEAR, RO_COPY, RO_EQUIV, RO_INVERT, RO_NAND, RO_NOR, RO_NO_OP, RO_OR, RO_OR_INVERT, RO_OR_REVERSE, RO_SET, RO_SRC_INVERT, RO_XOR, }; class SGP_RGBColor { private: short int r; short int g; short int b; public: SGP_RGBColor (int r, int g, int b) : r(r), g(g), b(b) {} int getRed () const { return r; } int getGreen () const { return g; } int getBlue () const { return b; } }; #endif ctsim-7.0.0/include/filter.h0000644000175000017500000001461115253666241014745 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: filter.h ** Purpose: Signal filter header file ** Programmer: Kevin Rosenberg ** Date Started: June 2000 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef FILTER_H #define FILTER_H #include #include #ifdef HAVE_CONFIG_H #include "config.h" #endif #ifdef HAVE_FFTW #include #include #include #endif // CLASS IDENTIFICATION // SignalFilter A filter used to process signals // // CONTAINS // signal vector // // Can create either a time/spatial waveform or a frequency signal // Waveforms can be created either by direct calculation or by inverse fourier transform class SignalFilter { public: static const int FILTER_INVALID; static const int FILTER_ABS_BANDLIMIT; // filter times |x| static const int FILTER_ABS_SINC; static const int FILTER_ABS_G_HAMMING; static const int FILTER_ABS_HANNING; static const int FILTER_ABS_COSINE; static const int FILTER_BANDLIMIT; static const int FILTER_SINC; static const int FILTER_G_HAMMING; static const int FILTER_HANNING; static const int FILTER_COSINE; static const int FILTER_TRIANGLE; static const int DOMAIN_INVALID; static const int DOMAIN_FREQUENCY; static const int DOMAIN_SPATIAL; SignalFilter (const char* szFilterName, double dFilterMinimum, double dFilterMaximum, int nFilterPoints, double dBandwidth, double dFilterParam, const char* szDomainName); SignalFilter (const int idFilter, double dFilterMinimum, double dFilterMaximum, int nFilterPoints, double dBandwidth, double dFilterParam, const int idDomain); SignalFilter (const char* szFilterName, const char* szDomainName, double dBandwidth, double dFilterParam); ~SignalFilter (void); // The filter table, with its length. A span rather than a double*, so a // const method cannot hand out a writable pointer. std::span getFilter (void) const { return m_vecFilter; } bool fail(void) const {return m_fail;} const std::string& failMessage(void) const {return m_failMessage;} const std::string& nameFilter(void) const { return m_nameFilter;} const std::string& nameDomain(void) const { return m_nameDomain;} int idFilter(void) const { return m_idFilter;} int idDomain(void) const { return m_idDomain;} int getNFilterPoints (void) const { return m_nFilterPoints; } double getFilterMin(void) const {return m_dFilterMin;} double getFilterMax(void) const {return m_dFilterMax;} double getFilterIncrement(void) const {return m_dFilterInc;} void copyFilterData(double *pdFilter, const int iStart, const int nPoints) const; double response (double x); static double spatialResponse (int fType, double bw, double x, double param); static double frequencyResponse (int fType, double bw, double u, double param); static double spatialResponseAnalytic (int fType, double bw, double x, double param); static double spatialResponseCalc (int fType, double bw, double x, double param, int nIntegral); static void setNumIntegral(int nIntegral) {N_INTEGRAL = nIntegral;} static int getFilterCount() {return s_iFilterCount;} static int getReconstructFilterCount() { return s_iReconstructFilterCount; } static const char* const* getFilterNameArray() {return s_aszFilterName;} static const char* const* getFilterTitleArray() {return s_aszFilterTitle;} static int convertFilterNameToID (std::string_view name); static const char* convertFilterIDToName (const int idFilter); static const char* convertFilterIDToTitle (const int idFilter); static int getDomainCount() {return s_iDomainCount;} static const char* const* getDomainNameArray() {return s_aszDomainName;} static const char* const* getDomainTitleArray() {return s_aszDomainTitle;} static int convertDomainNameToID (std::string_view name); static const char* convertDomainIDToName (const int idDomain); static const char* convertDomainIDToTitle (const int idDomain); static double sinc (double x) { return (fabs(x) > F_EPSILON ? (sin (x) / x) : 1.0); } static double sinc (double x, double mult) { return (fabs(x) > F_EPSILON ? (sin (x * mult) / x) : 1.0); } private: int m_nFilterPoints; double m_dBandwidth; double m_dFilterParam; double m_dFilterInc; double m_dFilterMin; double m_dFilterMax; std::vector m_vecFilter; std::string m_nameFilter; std::string m_nameDomain; int m_idFilter; int m_idDomain; bool m_fail; std::string m_failMessage; static const char* const s_aszFilterName[]; static const char* const s_aszFilterTitle[]; static const int s_iFilterCount; static const int s_iReconstructFilterCount; static const char* const s_aszDomainName[]; static const char* const s_aszDomainTitle[]; static const int s_iDomainCount; static int N_INTEGRAL; static bool haveAnalyticSpatial (const int filterID); void init (const int idFilter, double dFilterMin, double dFilterMax, int nFilterPoints, double dBandwidth, double dFilterParam, const int idDomain); void createFrequencyFilter (double* x) const; void createSpatialFilter (double* x) const; double spatialResponseCalc (double x) const; double spatialResponseAnalytic (double x) const; double frequencyResponse (double u) const; static double integral_abscos (double u, double w) { return (fabs (u) > F_EPSILON ? (cos (u * w) - 1) / (u * u) + w / u * sin (u * w) : (w * w / 2)); } }; #endif ctsim-7.0.0/include/hashtable.h0000644000175000017500000000441415253543770015414 0ustar kevinkevin/* FILE IDENTIFICATION ** ** File Name: hashtable.h ** Author: Kevin Rosenberg ** Purpose: Header file for hash table library ** Date Started: Dec. 2000 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef HASHTABLE_H #define HASHTABLE_H #include #include #include #include class KeywordCodeEntry { private: std::string m_strKeyword; int m_iCode; public: KeywordCodeEntry (std::string_view svKeyword, int iCode); const char* getKeyword() const { return m_strKeyword.c_str(); } bool matchesKeyword (std::string_view svMatch) const; int getCode () const { return m_iCode; } void setCode (int iCode) { m_iCode = iCode; } }; class KeywordCodeHashTable { public: enum { HASHSIZE = 100, }; KeywordCodeHashTable() { initTable(); } ~KeywordCodeHashTable() { freeTable(); } void installKeywordCode (std::string_view svKeyword, int iCode); KeywordCodeEntry* lookup (std::string_view svKeyword); private: // Each bucket owns its entries. It was a chain of raw pointers linked // through the entries themselves, walked and deleted by hand; a pointer // handed out by lookup() stays valid because the entry lives on the heap // and the vector holds only the handle to it. std::vector>> m_vecBuckets; int hash (std::string_view s); void initTable (); void freeTable (); }; #endif ctsim-7.0.0/include/ctglobals.h0000644000175000017500000000246115253666241015432 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: ctglobals.h ** Purpose: Global variables for CTSim ** Programmer: Kevin Rosenberg ** Date Started: Jan 2001 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef __CTGLOBALS_H #define __CTGLOBALS_H // g_bRunningWXWindows moved to the graphical program (src/ctsim.h). // Libraries no longer branch on which GUI is linked; see ctsupport_hooks.h. #endif ctsim-7.0.0/include/imagefile.h0000644000175000017500000002335315253543770015406 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: imagefile.h ** Purpose: imagefile class header ** Programmer: Kevin Rosenberg ** Date Started: June 2000 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef IMAGEFILE_H #define IMAGEFILE_H #include #include #include #include #include #include #include "ctsupport.h" #include "fnetorderstream.h" #include "array2dfile.h" class F32Image : public Array2dFile { public: F32Image (int nx, int ny, int dataType = Array2dFile::DATA_TYPE_REAL); F32Image (void); // Pixels are double whatever the on-disk sample size; the planes are // views onto one contiguous buffer (7.0.0~m8). Array2dPlane getArray (void) { return realPlane(); } Array2dPlaneConst getArray (void) const { return realPlane(); } Array2dPlane getImaginaryArray (void) { return imagPlane(); } Array2dPlaneConst getImaginaryArray (void) const { return imagPlane(); } private: F32Image (const F32Image& rhs); F32Image& operator= (const F32Image& rhs); }; class F64Image : public Array2dFile { public: F64Image (int nx, int ny, int dataType = Array2dFile::DATA_TYPE_REAL); F64Image (void); Array2dPlane getArray (void) { return realPlane(); } Array2dPlaneConst getArray (void) const { return realPlane(); } Array2dPlane getImaginaryArray (void) { return imagPlane(); } Array2dPlaneConst getImaginaryArray (void) const { return imagPlane(); } private: F64Image (const F64Image& rhs); F64Image& operator= (const F64Image& rhs); }; // 7.0.0~m8: pixels live in one std::vector per plane. These aliases // are views onto it, so the historical usage // ImageFileArray v = im.getArray(); v[ix][iy] = ...; // keeps working unchanged at every call site; v(ix, iy) is the form to // prefer in new code. using ImageFileBase = F32Image; // on-disk sample size unchanged (float32) using ImageFileValue = double; using ImageFileColumn = double*; using ImageFileColumnConst = const double*; // Views, not references: the pixels are one buffer and these address it. // "ImageFileArray v = im.getArray(); v[ix][iy] = ..." is unchanged. using ImageFileArray = Array2dPlane; using ImageFileArrayConst = Array2dPlaneConst; class ImageFile : public ImageFileBase { private: static const char* s_aszExportFormatName[]; static const char* s_aszExportFormatTitle[]; static const int s_iExportFormatCount; static const char* s_aszImportFormatName[]; static const char* s_aszImportFormatTitle[]; static const int s_iImportFormatCount; static void skipSpacePPM (FILE* fp); // skip space in a ppm file static bool readIntPPM (FILE* fp, int& iValue); // checked integer read public: static const int EXPORT_FORMAT_INVALID; static const int IMPORT_FORMAT_INVALID; static const int EXPORT_FORMAT_TEXT; static const int EXPORT_FORMAT_PGM; static const int EXPORT_FORMAT_PGMASCII; static const int IMPORT_FORMAT_PPM; #if HAVE_PNG static const int EXPORT_FORMAT_PNG; static const int EXPORT_FORMAT_PNG16; static const int IMPORT_FORMAT_PNG; #endif #if HAVE_DCMTK static const int EXPORT_FORMAT_DICOM; static const int IMPORT_FORMAT_DICOM; #endif static const int EXPORT_FORMAT_RAW; static int getExportFormatCount() {return s_iExportFormatCount;} static const char** getExportFormatNameArray() {return s_aszExportFormatName;} static const char** getExportFormatTitleArray() {return s_aszExportFormatTitle;} static int convertExportFormatNameToID (std::string_view name); static const char* convertExportFormatIDToName (const int ExportFormatID); static const char* convertExportFormatIDToTitle (const int ExportFormatID); static int getImportFormatCount() {return s_iImportFormatCount;} static const char** getImportFormatNameArray() {return s_aszImportFormatName;} static const char** getImportFormatTitleArray() {return s_aszImportFormatTitle;} static int convertImportFormatNameToID (std::string_view name); static const char* convertImportFormatIDToName (const int ImportFormatID); static const char* convertImportFormatIDToTitle (const int ImportFormatID); static const double s_dRedGrayscaleFactor; static const double s_dGreenGrayscaleFactor; static const double s_dBlueGrayscaleFactor; ImageFile (int nx, int ny) : ImageFileBase (nx, ny) {} ImageFile (void) : ImageFileBase () {} void getCenterCoordinates (unsigned int& iXCenter, unsigned int& iYCenter); bool convertRealToComplex (); bool convertComplexToReal (); void filterResponse (std::string_view domainName, double bw, std::string_view filterName, double filt_param, double dInputScale = 1., double dOutputScale = 1.); void statistics (double& min, double& max, double& mean, double& mode, double& median, double& stddev) const; void statistics (ImageFileArrayConst v, double& min, double& max, double& mean, double& mode, double& median, double& stddev) const; void getMinMax (double& min, double& max) const; void printStatistics (std::ostream& os) const; bool comparativeStatistics (const ImageFile& imComp, double& d, double& r, double& e) const; // The three measures written the same way everywhere they are reported -- // if2, the graphical comparison, and a script's expect-compare -- // so a number printed by one can be compared with a number printed by // another. Nine significant digits: a double carries about sixteen, and // six was too few to tell two reconstructions apart. // // The names matter: d and r are error measures, but e is not. It is the // worst 2x2 neighbourhood, from Herman's book, and calling it an error // invites it to be read as one. static std::string formatComparativeStatistics (double d, double r, double e); // Each measure has a short label, used where the numbers are reported, and // a full name, used where there is room to explain what is being measured. // The short labels are what a person scanning a column of output reads, and // what a harness matches on. static const char* labelOfDistanceMeasure(); // "NormRMS" static const char* labelOfAbsoluteMeasure(); // "NormMeanAbs" static const char* labelOfWorstCaseMeasure(); // "Worst2x2" static const char* nameOfDistanceMeasure(); // "normalized root mean squared distance" static const char* nameOfAbsoluteMeasure(); // "normalized mean absolute distance" static const char* nameOfWorstCaseMeasure(); // "worst case 2x2 neighbourhood distance" bool printComparativeStatistics (const ImageFile& imComp, std::ostream& os) const; bool subtractImages (const ImageFile& rRHS, ImageFile& result) const; bool addImages (const ImageFile& rRHS, ImageFile& result) const; bool multiplyImages (const ImageFile& rRHS, ImageFile& result) const; bool divideImages (const ImageFile& rRHS, ImageFile& result) const; bool scaleImage (ImageFile& result) const; bool invertPixelValues (ImageFile& result) const; bool sqrt (ImageFile& result) const; bool square (ImageFile& result) const; bool log (ImageFile& result) const; bool exp (ImageFile& result) const; bool fourier (ImageFile& result) const; bool inverseFourier (ImageFile& result) const; #ifdef HAVE_FFTW bool fft (ImageFile& result) const; bool ifft (ImageFile& result) const; // In-place transforms: these modify this image's own pixels and convert it // to complex if needed. Before 6.2.0 they took an ImageFile& that every // caller had to alias to *this (rIF.fftRows (rIF)); the parameter is gone. bool fftRows (); bool ifftRows (); bool fftCols (); bool ifftCols (); #endif bool magnitude (ImageFile& result) const; bool phase (ImageFile& result) const; bool real (ImageFile& result) const; bool imaginary (ImageFile& result) const; bool exportImage (std::string_view format, std::string_view filename, int nxcell, int nycell, double densmin, double densmax); bool importImage (std::string_view format, std::string_view filename); #ifdef HAVE_PNG bool writeImagePNG (std::string_view outfile, int bitdepth, int nxcell, int nycell, double densmin, double densmax); bool readImagePNG (std::string_view filename); #endif bool writeImagePGM (std::string_view outfile, int nxcell, int nycell, double densmin, double densmax); bool writeImagePGMASCII (std::string_view outfile, int nxcell, int nycell, double densmin, double densmax); bool readImagePPM (std::string_view filename); bool writeImageRaw (std::string_view outfile, int nxcell, int nycell); bool writeImageText (std::string_view outfile); static double redGrayscaleFactor() {return s_dRedGrayscaleFactor;} static double greenGrayscaleFactor() {return s_dGreenGrayscaleFactor;} static double blueGrayscaleFactor() {return s_dBlueGrayscaleFactor;} static double colorToGrayscale (double r, double g, double b) { return r * s_dRedGrayscaleFactor + g * s_dGreenGrayscaleFactor + b * s_dBlueGrayscaleFactor; } }; #endif ctsim-7.0.0/include/array2d.h0000644000175000017500000000606515253666241015030 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: array2d.h ** Purpose: 2-dimension array classes ** Programmer: Kevin Rosenberg ** Date Started: June 2000 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef ARRAY2D_H #define ARRAY2D_H #include "ctsupport.h" #include // storage is std::vector, so the destructor is no longer hand written. // Until 6.3.x this class allocated a T*[nx] of T[ny] rows and released each row // with a scalar delete, which is undefined behaviour; with 1024-detector // reconstructions it corrupted the heap and crashed pjrec (fixed in 6.2.1, but // only by correcting the delete). A column pointer table is kept so that // getArray() can still hand out a T** to the existing call sites. template class Array2d { public: Array2d (unsigned int x, unsigned int y) : m_nx(0), m_ny(0) { initSetSize (x, y); } Array2d () : m_nx(0), m_ny(0) {} ~Array2d () {} void initSetSize (unsigned int x, unsigned int y) { m_nx = x; m_ny = y; m_vecData.assign (m_nx, std::vector (m_ny, T())); m_vecColumns.resize (m_nx); for (unsigned int i = 0; i < m_nx; i++) m_vecColumns[i] = m_vecData[i].data(); } T** getArray () const { return m_nx ? const_cast(m_vecColumns.data()) : nullptr; } T* getColumn (unsigned int x) const { return (x < m_nx) ? const_cast(m_vecData[x].data()) : nullptr; } T getPoint (unsigned int x, unsigned int y) const { return (x < m_nx && y < m_ny) ? m_vecData[x][y] : T(); } unsigned int sizeofPixel () const { return sizeof(T); } unsigned int sizeofColumn () const { return (sizeof(T) * m_ny); } unsigned int sizeofArray () const { return (sizeof(T) * m_nx * m_ny); } private: unsigned int m_nx; unsigned int m_ny; std::vector > m_vecData; std::vector m_vecColumns; // non-owning view, for the T** accessor Array2d (const Array2d& rhs); // the column table would dangle Array2d& operator= (const Array2d& rhs); }; #endif ctsim-7.0.0/include/fnetorderstream.h0000644000175000017500000000706615253543770016673 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: fnetorderstream.h ** Purpose: Network-order file stream header ** Programmer: Kevin Rosenberg ** Date Started: Sep 2000 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef NETORDER_H #define NETORDER_H #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include inline bool NativeBigEndian (void) { #ifdef WORDS_BIGENDIAN return true; #else return false; #endif } inline void SwapBytes2 (void* buffer) { unsigned char* p = static_cast(buffer); unsigned char temp = p[0]; p[0] = p[1]; p[1] = temp; } // 0<->3 1<->2 = 0123 -> 3210 inline void SwapBytes4 (void* buffer) { unsigned char* p = static_cast(buffer); unsigned char temp = p[0]; p[0] = p[3]; p[3] = temp; temp = p[1]; p[1] = p[2]; p[2] = temp; } // 0<->7 1<->6 2<->5 3<->4 = 01234567 -> 76543210 inline void SwapBytes8 (void* buffer) { unsigned char* p = static_cast(buffer); unsigned char temp = p[0]; p[0] = p[7]; p[7] = temp; temp = p[1]; p[1] = p[6]; p[6] = temp; temp = p[2]; p[2] = p[5]; p[5] = temp; temp = p[3]; p[3] = p[4]; p[4] = temp; } inline void SwapBytes2IfLittleEndian (void* buffer) { #ifndef WORDS_BIGENDIAN SwapBytes2 (buffer); #endif } inline void SwapBytes4IfLittleEndian (void* buffer) { #ifndef WORDS_BIGENDIAN SwapBytes4 (buffer); #endif } inline void SwapBytes8IfLittleEndian (void* buffer) { #ifndef WORDS_BIGENDIAN SwapBytes8 (buffer); #endif } void ConvertNetworkOrder (void* buffer, size_t bytes); void ConvertReverseNetworkOrder (void* buffer, size_t bytes); using std::fstream; class fnetorderstream : public fstream { public: fnetorderstream (const char* filename, std::ios::openmode mode) : fstream (filename, mode) {} ~fnetorderstream (void) {} virtual void writeInt16 (kuint16 n); virtual void writeInt32 (kuint32 n); virtual void writeFloat32 (kfloat32 n); virtual void writeFloat64 (kfloat64 n); virtual void readInt16 (kuint16& n); virtual void readInt32 (kuint32& n); virtual void readFloat32 (kfloat32& n); virtual void readFloat64 (kfloat64& n); }; class frnetorderstream : public fnetorderstream { public: frnetorderstream (const char* filename, std::ios::openmode mode) : fnetorderstream (filename, mode) {} virtual void writeInt16 (kuint16 n); virtual void writeInt32 (kuint32 n); virtual void writeFloat32 (kfloat32 n); virtual void writeFloat64 (kfloat64 n); virtual void readInt16 (kuint16& n); virtual void readInt32 (kuint32& n); virtual void readFloat32 (kfloat32& n); virtual void readFloat64 (kfloat64& n); }; #endif ctsim-7.0.0/include/fileformat.h0000644000175000017500000000434215253666241015610 0ustar kevinkevin/***************************************************************************** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef CTSIM_FILEFORMAT_H #define CTSIM_FILEFORMAT_H #include #include // HDF5 is the default format for newly written image and projection // files. Reading detects the format from the file itself, so the legacy // binaries keep working and no flag is needed to open them. enum CTSimFileFormat { CTSIM_FORMAT_LEGACY, // the original CTSim binary .if / .pj CTSIM_FORMAT_HDF5 }; // True when the file begins with the HDF5 signature. Works even in a build // without HDF5 support, so such a build can report the situation properly // rather than failing to parse. bool ctsimFileIsHDF5 (std::string_view svFilename); // True when this build can actually read and write HDF5. bool ctsimHaveHDF5 (); // What a file holds, determined from the file itself: the HDF5 "content" // attribute, or the two-byte signature of a legacy file. Lets a caller pick // the right reader instead of trying each and emitting errors on the way. enum CTSimFileContent { CTSIM_CONTENT_UNKNOWN, CTSIM_CONTENT_IMAGE, CTSIM_CONTENT_PROJECTIONS, CTSIM_CONTENT_PLOT // text .plt; recognized by its tag (7.0.0) }; CTSimFileContent ctsimFileContent (std::string_view svFilename); CTSimFileFormat ctsimDefaultFileFormat (); void ctsimSetDefaultFileFormat (CTSimFileFormat format); #endif ctsim-7.0.0/include/reconstruct.h0000644000175000017500000000510115253543770016026 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: reconstruct.h Header file for Reconstruction class ** Programmer: Kevin Rosenberg ** Date Started: Aug 84 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef __RECONSTRUCT_H #define __RECONSTRUCT_H #include #include class Projections; class ImageFile; class Backprojector; class ProcessSignal; #include #include struct ReconstructionROI { double m_dXMin; double m_dYMin; double m_dXMax; double m_dYMax; }; class Reconstructor { private: const Projections& m_rOriginalProj; const Projections* m_pProj; ImageFile& m_rImagefile; ProcessSignal* m_pProcessSignal; Backprojector* m_pBackprojector; int m_nFilteredProjections; int m_iTrace; const bool m_bRebinToParallel; bool m_bFail; std::string m_strFailMessage; std::vector m_vecPlotXAxis; public: Reconstructor (const Projections& rProj, ImageFile& rIF, std::string_view filterName, double filt_param, std::string_view filterMethodName, const int zeropad, std::string_view filterGenerationName, std::string_view interpName, int interpFactor, std::string_view backprojectName, const int trace, ReconstructionROI* pROI = nullptr, bool bRebinToParallel = false, SGP* pSGP = nullptr); ~Reconstructor (); bool fail() const {return m_bFail;} const std::string& failMessage() const {return m_strFailMessage;} void plotFilter (SGP* pSGP = nullptr); void reconstructAllViews (); void reconstructView (int iStartView = 0, int iViewCount = -1, SGP* pSGP = nullptr, bool bBackprojectView = true, double dGraphWidth = 1.); void postProcessing (); }; #endif ctsim-7.0.0/include/scanner.h0000644000175000017500000001770515253543770015121 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: scanner.h ** Purpose: Scanner header file ** Programmer: Kevin Rosenberg ** Date Started: July 1, 1984 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef SCANNER_H #define SCANNER_H #include #include #include #include #include "trace.h" class Projections; class Phantom; class PhantomElement; class SGP; // Projections are collected along an array of ndet detectors. The data // for these detectors is stored in the class DetectorArray using DetectorValue = float; class DetectorArray { public: DetectorArray (const int ndet); ~DetectorArray (); int nDet() const {return m_nDet;} double viewAngle() const {return m_viewAngle;} // Spans, so the values travel with their count. The array owns a vector // rather than a new[] that had to be matched with a delete[]. std::span detValues() { return m_vecDetValues; } std::span detValues() const { return m_vecDetValues; } void setViewAngle (double viewAngle) { m_viewAngle = viewAngle; } private: std::vector m_vecDetValues; // what the detectors recorded int m_nDet; // Number of detectors in array */ double m_viewAngle; // View angle in radians DetectorArray& operator=(const DetectorArray& rhs); // assignment DetectorArray (const DetectorArray& rhs); // copy constructor }; class Scanner { public: static const int GEOMETRY_INVALID; static const int GEOMETRY_PARALLEL; static const int GEOMETRY_EQUILINEAR; static const int GEOMETRY_EQUIANGULAR; Scanner (const Phantom& phm, std::string_view geometryName, int nDet, int nView, int iOffsetView, int nSample, const double rot_anglen, double dFocalLengthRatio, double dCenterDetectorRatio, double dViewRatio, double dScanRatio); ~Scanner(); void collectProjections (Projections& proj, const Phantom& phm, const int trace = Trace::TRACE_NONE, SGP* pSGP = nullptr); void collectProjections (Projections& proj, const Phantom& phm, const int iStartView, const int iNumViews, const int iOffsetView, bool bStoreAtViewPosition, const int trace = Trace::TRACE_NONE, SGP* pSGP = nullptr); void collectProjections (Projections& proj, const Phantom& phm, const int iStartView, const int iNumViews, const int iOffsetView, int iStorageOffset, const int trace = Trace::TRACE_NONE, SGP* pSGP = nullptr); void setNView (int nView); void setOffsetView (int iOffsetView); bool fail() const {return m_fail;} const std::string& failMessage() const {return m_failMessage;} unsigned int nDet() const {return m_nDet;} unsigned int nView() const {return m_nView;} unsigned int offsetView() const {return m_iOffsetView;} unsigned int startView() const {return m_startView;} double rotInc() const {return m_rotInc;} double detInc() const {return m_detInc;} double detLen() const {return m_detLen;} double detStart() const {return m_detStart;} double focalLength() const {return m_dFocalLength;} double sourceDetectorLength() const {return m_dSourceDetectorLength;} double centerDetectorLength() const {return m_dCenterDetectorLength;} double viewDiameter() const {return m_dViewDiameter;} double scanDiameter() const {return m_dScanDiameter;} double fanBeamAngle() const {return m_dFanBeamAngle;} int geometry() const {return m_idGeometry;} static int getGeometryCount() {return s_iGeometryCount;} static const char* const* getGeometryNameArray() {return s_aszGeometryName;} static const char* const* getGeometryTitleArray() {return s_aszGeometryTitle;} static int convertGeometryNameToID (std::string_view name); static const char* convertGeometryIDToName (const int idGeometry); static const char* convertGeometryIDToTitle (const int idGeometry); private: bool m_fail; std::string m_failMessage; int m_idGeometry; unsigned int m_nDet; /* Number of detectors in array */ unsigned int m_nView; /* Number of rotated views */ unsigned int m_iOffsetView; unsigned int m_startView; unsigned int m_nSample; /* Number of rays per detector */ double m_dFocalLength; // Focal Length, distance from source to center double m_dSourceDetectorLength; // Distance from source to detectors double m_dCenterDetectorLength; // Distance from center to detectors double m_dViewDiameter; // Diameter of area being processed double m_dScanDiameter; // Diamer of area being scanned double m_dViewRatio; // View Ratio to diameter phantom double m_dFocalLengthRatio; // Source to Center Length as ratio to viewDiameter radius double m_dCenterDetectorRatio; // Center to Detector Length as ratio of viewDiameter radius double m_dScanRatio; // Scan length to view length ratio double m_dFanBeamAngle; double m_detLen; // Total length of detector array double m_rotLen; // Rotation angle length in radians (norm 2PI) double m_detInc; // Increment between centers of detectors double m_rotInc; // Increment in rotation angle between views double m_detStart; double m_dXCenter; // Center of Phantom double m_dYCenter; double m_dAngularDetIncrement; double m_dAngularDetLen; int m_trace; struct { double xd1,yd1,xd2,yd2; /* Coordinates of detector endpoints */ double xs1,ys1,xs2,ys2; /* Coordinates of source endpoints */ double angle; /* Starting angle */ double dAngularDet; } m_initPos; GRFMTX_2D m_rotmtxIncrement; #ifdef HAVE_SGP SGP* m_pSGP; // Pointer to graphics device double m_dXMinWin; // Extent of graphics window double m_dXMaxWin; double m_dYMinWin; double m_dYMaxWin; double m_dTextHeight; #endif static const char* const s_aszGeometryName[]; static const char* const s_aszGeometryTitle[]; static const int s_iGeometryCount; void projectSingleView (const Phantom& phm, DetectorArray& darray, const double xd1, const double yd1, const double xd2, const double yd2, const double xs1, const double ys1, const double xs2, const double ys2, const double dDetAngle); double projectSingleLine (const Phantom& phm, const double x1, const double y1, const double x2, const double y2); double projectLineAgainstPElem (const PhantomElement& pelem, const double x1, const double y1, const double x2, const double y2); void traceShowParam (const char* szLabel, const char *fmt, int row, int color, ...); void traceShowParamXOR (const char* szLabel, const char *fmt, int row, int color, ...); void traceShowParamRasterOp (int iRasterOp, const char* szLabel, const char* fmt, int row, int color, va_list va); }; const static int PROJECTION_TRACE_ROW_PHANT_ID=0; const static int PROJECTION_TRACE_ROW_GEOMETRY=1; const static int PROJECTION_TRACE_ROW_FOCAL_LENGTH=2; const static int PROJECTION_TRACE_ROW_FIELD_OF_VIEW=3; const static int PROJECTION_TRACE_ROW_NDET=4; const static int PROJECTION_TRACE_ROW_NVIEW=5; const static int PROJECTION_TRACE_ROW_SAMPLES=6; const static int PROJECTION_TRACE_ROW_CURR_VIEW=7; const static int PROJECTION_TRACE_ROW_ATTEN=8; #endif ctsim-7.0.0/include/pngmeta.h0000644000175000017500000000437315253543770015120 0ustar kevinkevin/***************************************************************************** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef PNGMETA_H #define PNGMETA_H #include #include #include // What a PNG file says about itself (M6b): the header fields and the // optional chunks a viewer would want to show -- gamma, physical resolution, // text. Read without decoding the pixels, so it is cheap for any size. #include #include struct PNGMetadata { int iWidth = 0; int iHeight = 0; int iBitDepth = 0; std::string strColorType; // "grayscale", "RGB", "palette", "grayscale+alpha", "RGBA" int iChannels = 0; bool bInterlaced = false; bool bHasGamma = false; double dGamma = 0.; bool bHasResolution = false; // pHYs double dPixelsPerMetreX = 0.; double dPixelsPerMetreY = 0.; bool bHasBackground = false; bool bHasTransparency = false; // tRNS bool bHasICCProfile = false; bool bHasSRGB = false; std::vector > vecText; // tEXt / zTXt / iTXt, key and value std::int64_t lFileBytes = 0; // long is 32 bits on Windows }; // True if the file begins with the PNG signature. bool pngFileIsPNG (std::string_view svFilename); // Reads the header and ancillary chunks. False, with strError set, if the // file is not a PNG or cannot be read. bool pngReadMetadata (std::string_view svFilename, PNGMetadata& meta, std::string& strError); #endif ctsim-7.0.0/include/array2dfile.h0000644000175000017500000003364415253666241015673 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: array2dfile.h ** Purpose: 2-dimension array file class ** Programmer: Kevin Rosenberg ** Date Started: June 2000 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef ARRAY2DFILE_H #define ARRAY2DFILE_H #include #include #include #include #include #include #include #include #include #include #include #include "ctsupport.h" #include "fnetorderstream.h" #include "array2d.h" // parameters of the operation that produced a file, recorded as // key/value pairs so that two files can be compared by what made them rather // than by reading prose. The human-readable label string is composed from // these, so existing displays are unchanged. class HistoryParams { public: // string_view takes a literal, a std::string or a char* without the // caller converting, so one overload serves for all three. HistoryParams& add (std::string_view key, std::string_view value); // A literal would otherwise go to the bool overload below -- a pointer // converts to bool by a standard conversion, which beats the user-defined // one to string_view -- and every string value would be recorded as // "true". This overload is an exact match and takes precedence. HistoryParams& add (std::string_view key, const char* value) { return add (key, std::string_view (value ? value : "")); } HistoryParams& add (std::string_view key, double dValue); HistoryParams& add (std::string_view key, int iValue); HistoryParams& add (std::string_view key, bool bValue); const std::vector >& get() const { return m_vecParams; } bool empty() const { return m_vecParams.empty(); } // "key=value, key=value", for the human-readable label text. std::string toString() const; // "key=value" per line, the form stored in HDF5. std::string encode() const; void decode (const std::string& strEncoded); private: std::vector > m_vecParams; }; class Array2dFileLabel { public: enum { L_EMPTY = 0, L_HISTORY = 1, L_USER = 2, }; Array2dFileLabel(); Array2dFileLabel (std::string_view str, double ctime = 0.); Array2dFileLabel (const int type, std::string_view str, double ctime = 0.); ~Array2dFileLabel(); const std::string& getLabelString (void) const { return m_strLabel; } kfloat64 getCalcTime (void) const { return m_calcTime; } void setCalcTime (kfloat64 calcTime) { m_calcTime = calcTime; } void setLabelType (int labelType) { m_labelType = labelType; } int getLabelType (void) const { return m_labelType; } std::string& setLabelString (std::string_view str) { m_strLabel = str; return m_strLabel; } // structured description of the operation that produced the file. void setOperation (std::string_view operation) { m_strOperation = operation; } const std::string& getOperation() const { return m_strOperation; } // Which CTSim wrote this entry. A file can pass through several versions, // so this belongs on the entry rather than only on the file. void setVersion (std::string_view version) { m_strVersion = version; } const std::string& getVersion() const { return m_strVersion; } void setParameters (const HistoryParams& params) { m_params = params; } const HistoryParams& getParameters() const { return m_params; } void setDateTime (int year, int month, int day, int hour, int minute, int second); // Minutes east of UTC when the label was made, so a time written in one // zone still reads correctly in another. Files that predate this field // read back with the local offset, which is what they always meant. void setUTCOffsetMinutes (int iMinutes) { m_iUTCOffsetMinutes = iMinutes; } int getUTCOffsetMinutes () const { return m_iUTCOffsetMinutes; } void getDateTime (int& year, int& month, int& day, int& hour, int& minute, int& second) const; const std::string& getDateString () const; void print (std::ostream& os) const; void printBrief (std::ostream& os) const; void printBrief (std::ostringstream& os) const; Array2dFileLabel (const Array2dFileLabel& rhs); Array2dFileLabel& operator= (const Array2dFileLabel& rhs); private: void init (void); std::string m_strOperation; std::string m_strVersion; HistoryParams m_params; kuint16 m_labelType; int m_iUTCOffsetMinutes; kuint16 m_year; kuint16 m_month; kuint16 m_day; kuint16 m_hour; kuint16 m_minute; kuint16 m_second; std::string m_strLabel; kfloat64 m_calcTime; mutable std::string m_strDate; }; // A pixel plane, addressed as v[ix][iy] or v(ix, iy). // // 7.0.0~m8: the pixels are one allocation of nx*ny doubles, column-major, in // place of a vector of nx column vectors. A 768 x 768 image was 768 scattered // blocks; one block is what the cache, FFTW and HDF5 all want. These views // are what getArray() hands out: operator[] returns a pointer into the plane, // so every "v[ix][iy]" written against the old storage compiles and means the // same thing, and operator() is the form to write from now on. class Array2dPlane { public: Array2dPlane (double* pData = nullptr, size_t nx = 0, size_t ny = 0) : m_pData (pData), m_nx (nx), m_ny (ny) {} double* operator[] (size_t ix) const { return m_pData + ix * m_ny; } double& operator() (size_t ix, size_t iy) const { return m_pData[ix * m_ny + iy]; } double* data() const { return m_pData; } // the whole plane, contiguous size_t nx() const { return m_nx; } size_t ny() const { return m_ny; } size_t size() const { return m_nx * m_ny; } bool isNull() const { return m_pData == nullptr; } bool empty() const { return m_pData == nullptr || m_nx == 0 || m_ny == 0; } explicit operator bool() const { return m_pData != nullptr; } private: double* m_pData; size_t m_nx; size_t m_ny; }; class Array2dPlaneConst { public: Array2dPlaneConst (const double* pData = nullptr, size_t nx = 0, size_t ny = 0) : m_pData (pData), m_nx (nx), m_ny (ny) {} Array2dPlaneConst (const Array2dPlane& rhs) : m_pData (rhs.data()), m_nx (rhs.nx()), m_ny (rhs.ny()) {} const double* operator[] (size_t ix) const { return m_pData + ix * m_ny; } const double& operator() (size_t ix, size_t iy) const { return m_pData[ix * m_ny + iy]; } const double* data() const { return m_pData; } size_t nx() const { return m_nx; } size_t ny() const { return m_ny; } size_t size() const { return m_nx * m_ny; } bool isNull() const { return m_pData == nullptr; } bool empty() const { return m_pData == nullptr || m_nx == 0 || m_ny == 0; } explicit operator bool() const { return m_pData != nullptr; } private: const double* m_pData; size_t m_nx; size_t m_ny; }; class Array2dFile { public: enum { PIXEL_INVALID = 0, PIXEL_INT8 = 1, PIXEL_UINT8 = 2, PIXEL_INT16 = 3, PIXEL_UINT16 = 4, PIXEL_INT32 = 5, PIXEL_UINT32 = 6, PIXEL_FLOAT32 = 7, PIXEL_FLOAT64 = 8, }; enum { DATA_TYPE_INVALID = 0, DATA_TYPE_REAL, DATA_TYPE_COMPLEX, }; Array2dFile (int nx, int ny, int pixelSize, int pixelFormat = PIXEL_INVALID, int dataType = DATA_TYPE_REAL); Array2dFile (void); ~Array2dFile (); void setArraySize (int nx, int ny, int pixelSize, int pixelFormat = PIXEL_INVALID, int dataType = DATA_TYPE_REAL); void setArraySize (int nx, int ny); unsigned int getNumLabels (void) const { return m_labels.size(); } const Array2dFileLabel& labelGet (int label_num) const; void labelAdd (const Array2dFileLabel& label); void labelAdd (std::string_view label, double calc_time = 0.); void labelAdd (int type, std::string_view label, double calc_time = 0.); // preferred way to record history. Composes the human-readable text // from the operation and parameters, so every tool records the same detail. void addHistory (std::string_view operation, const HistoryParams& params, double dCalcTime = 0.); // extensible metadata. Unknown keys read from a file are kept and // written back out, so a file produced by a later version does not lose // fields when an older tool rewrites it. Stored only in HDF5; the legacy // writer has nowhere to put them and warns. void setUserAttribute (std::string_view name, std::string_view value); void setUserAttribute (std::string_view name, double dValue); bool getUserAttribute (std::string_view name, std::string& strValue) const; double getUserAttributeDouble (std::string_view name, double dDefault) const; const std::map>& userAttributes() const { return m_mapUser; } void clearUserAttributes() { m_mapUser.clear(); } void labelsCopy (const Array2dFile& file, std::string_view idStr = {}); void setPixelFormat (int type) { m_pixelFormat = type; } void setPixelSize (int size) { m_pixelSize = size; } kuint32 nx (void) const { return m_nx; } kuint32 ny (void) const { return m_ny; } // The sample format and size the file uses on disk. In memory every value // is a double (6.1.0); these describe storage, which differs between the // legacy format (32-bit float) and HDF5 (64-bit double). int pixelSize() const { return m_pixelSize; } int pixelFormat() const { return m_pixelFormat; } bool isComplex() const { return m_dataType == DATA_TYPE_COMPLEX; } bool isReal() const { return m_dataType == DATA_TYPE_REAL; } int dataType () const { return static_cast(m_dataType); } void setDataType (int dataType) { m_dataType = dataType; } void setAxisIncrement (double axisIncX, double axisIncY); bool reallocRealToComplex (); bool reallocComplexToReal (); void getPixelValueRange (double& pvmin, double& pvmax) const; void setAxisExtent (double minX, double maxX, double minY, double maxY); bool getAxisExtent (double& minX, double& maxX, double& minY, double& maxY) const { if (! m_axisExtentKnown) return false; minX = m_minX; maxX = m_maxX; minY = m_minY; maxY=m_maxY; return true; } void doPixelOffsetScale (double offset, double scale); kfloat64 axisIncrementX() const {return m_axisIncrementKnown ? m_axisIncrementX : 0.;} kfloat64 axisIncrementY() const {return m_axisIncrementKnown ? m_axisIncrementY : 0.;} void arrayDataClear (void); bool fileRead (std::string_view filename); // HDF5 storage. fileRead/fileWrite dispatch to these automatically, // so callers do not choose a format; reading detects it from the file. bool readHDF5 (std::string_view filename); bool writeHDF5 (std::string_view filename); bool fileWrite (std::string_view filename); const std::string& getFilename (void) const { return m_filename; } void printLabels (std::ostream& os) const; void printLabelsBrief (std::ostream& os) const; void printLabelsBrief (std::ostringstream& os) const; unsigned int nLabels() const { return m_labels.size(); } using labelIterator = std::vector::iterator; using constLabelIterator = std::vector::const_iterator; protected: using labelContainer = std::vector; static const kuint16 m_signature; kuint16 m_headersize; std::string m_filename; kuint16 m_pixelSize; kuint16 m_pixelFormat; kuint16 m_axisIncrementKnown; kfloat64 m_axisIncrementX, m_axisIncrementY; kuint16 m_axisExtentKnown; kfloat64 m_minX, m_maxX, m_minY, m_maxY; kfloat64 m_offsetPV, m_scalePV; kuint32 m_nx; kuint32 m_ny; kuint32 m_arraySize; labelContainer m_labels; std::map> m_mapUser; kuint16 m_numFileLabels; kuint16 m_dataType; // 7.0.0~m8: one allocation per plane, column-major, so m_real[ix*ny + iy] // is the pixel (ix, iy). It was a vector of column vectors until then. // The on-disk format is unchanged (m_pixelSize-byte samples; see // arrayDataRead/arrayDataWrite). Reach them through the planes below, // not directly. std::vector m_real; std::vector m_imag; protected: Array2dPlane realPlane() { return Array2dPlane (m_real.empty() ? nullptr : m_real.data(), m_nx, m_ny); } Array2dPlaneConst realPlane() const { return Array2dPlaneConst (m_real.empty() ? nullptr : m_real.data(), m_nx, m_ny); } Array2dPlane imagPlane() { return Array2dPlane (m_imag.empty() ? nullptr : m_imag.data(), m_nx, m_ny); } Array2dPlaneConst imagPlane() const { return Array2dPlaneConst (m_imag.empty() ? nullptr : m_imag.data(), m_nx, m_ny); } private: void init (void); bool headerRead (frnetorderstream& fs); bool headerWrite (frnetorderstream& fs); bool arrayDataRead (frnetorderstream& fs); bool arrayDataWrite (frnetorderstream& fs); bool labelsRead (frnetorderstream& fs); bool labelsWrite (frnetorderstream& fs); bool labelSeek (int label_num); void allocArrays (); void freeArrays (); // clears pixel storage (kept for source compatibility) void allocArray (unsigned char**& rppData); void freeArray (unsigned char**& rppData); Array2dFile (const Array2dFile& rhs); // copy constructor Array2dFile& operator= (const Array2dFile&); // assignment operator }; #endif ctsim-7.0.0/include/interpolator.h0000644000175000017500000002231115253543770016177 0ustar kevinkevin/***************************************************************************** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #include #include "array2dfile.h" // Array2dPlane, the pixel views #include #include "array2dfile.h" // Array2dPlane, the pixel views class CubicSplineInterpolator { private: std::vector m_vecY2; // second differential of y data const double* const m_pdY; const int m_n; public: CubicSplineInterpolator (const double* const y, int n); ~CubicSplineInterpolator (); double interpolate (double x); }; class CubicPolyInterpolator { private: const double* const m_pdY; const int m_n; public: CubicPolyInterpolator (const double* const y, int n); ~CubicPolyInterpolator (); double interpolate (double x); }; template class BilinearInterpolator { private: std::vector m_vecColumns; // used when constructed from vector storage T** m_ppMatrix; const int m_nx; const int m_ny; public: BilinearInterpolator (T** ppMatrix, unsigned int nx, unsigned int ny) : m_ppMatrix(ppMatrix), m_nx(nx), m_ny(ny) {} // 7.0.0~m8: images are one contiguous plane; build a column pointer table // into it so the interpolation code below is unchanged. The table costs // nx pointers once per interpolator, against a bilinear interpolation per // pixel, so it is not worth rewriting the arithmetic to index directly. BilinearInterpolator (const Array2dPlaneConst& plane, unsigned int nx, unsigned int ny) : m_vecColumns(nx), m_ppMatrix(nullptr), m_nx(nx), m_ny(ny) { for (unsigned int i = 0; i < nx; i++) m_vecColumns[i] = const_cast(plane[i]); m_ppMatrix = m_vecColumns.data(); } BilinearInterpolator (const Array2dPlane& plane, unsigned int nx, unsigned int ny) : m_vecColumns(nx), m_ppMatrix(nullptr), m_nx(nx), m_ny(ny) { for (unsigned int i = 0; i < nx; i++) m_vecColumns[i] = plane[i]; m_ppMatrix = m_vecColumns.data(); } T interpolate (double dXPos, double dYPos) { int iFloorX = static_cast(floor(dXPos)); int iFloorY = static_cast(floor (dYPos)); double dXFrac = dXPos - iFloorX; double dYFrac = dYPos - iFloorY; T result = 0; if (iFloorX < 0 || iFloorY < 0 || iFloorX > m_nx-1 || iFloorY > m_ny-1) result = 0; else if (iFloorX == m_nx - 1 && iFloorY == m_ny - 1) result = static_cast(m_ppMatrix[m_nx-1][m_ny-1]); else if (iFloorX == m_nx - 1) result = static_cast(m_ppMatrix[iFloorX][iFloorY] + dYFrac * (m_ppMatrix[iFloorX][iFloorY+1] - m_ppMatrix[iFloorX][iFloorY])); else if (iFloorY == m_ny - 1) result = static_cast(m_ppMatrix[iFloorX][iFloorY] + dXFrac * (m_ppMatrix[iFloorX+1][iFloorY] - m_ppMatrix[iFloorX][iFloorY])); else result = static_cast ((1 - dXFrac) * (1 - dYFrac) * m_ppMatrix[iFloorX][iFloorY] + dXFrac * (1 - dYFrac) * m_ppMatrix[iFloorX+1][iFloorY] + dYFrac * (1 - dXFrac) * m_ppMatrix[iFloorX][iFloorY+1] + dXFrac * dYFrac * m_ppMatrix[iFloorX+1][iFloorY+1]); return result; } }; template class BilinearPolarInterpolator { private: T** const m_ppMatrix; const int m_nAngle; const int m_nPos; int m_nCenterPos; public: BilinearPolarInterpolator (T** ppMatrix, unsigned int nAngle, unsigned int nPos) : m_ppMatrix(ppMatrix), m_nAngle(nAngle), m_nPos(nPos) { if (m_nPos %2) m_nCenterPos = (m_nPos - 1) / 2; else m_nCenterPos = m_nPos / 2; } T interpolate (double dAngle, double dPos) { int iFloorAngle = static_cast(floor(dAngle)); int iFloorPos = static_cast(floor (dPos)); double dAngleFrac = dAngle - iFloorAngle; double dPosFrac = dPos - iFloorPos; T result = 0; if (iFloorAngle < -1 || iFloorPos < 0 || iFloorAngle > m_nAngle-1 || iFloorPos > m_nPos-1) result = 0; else if (iFloorAngle == -1 && iFloorPos == m_nPos-1) result = static_cast(m_ppMatrix[0][m_nPos-1] + dAngleFrac * (m_ppMatrix[m_nAngle-1][iFloorPos] - m_ppMatrix[0][iFloorPos])); else if (iFloorAngle == m_nAngle - 1 && iFloorPos == m_nPos-1) result = static_cast(m_ppMatrix[m_nAngle-1][m_nPos-1] + dAngleFrac * (m_ppMatrix[0][iFloorPos] - m_ppMatrix[m_nAngle-1][iFloorPos])); else if (iFloorPos == m_nPos - 1) result = static_cast(m_ppMatrix[iFloorAngle][iFloorPos] + dAngleFrac * (m_ppMatrix[iFloorAngle+1][iFloorPos] - m_ppMatrix[iFloorAngle][iFloorPos])); else { if (iFloorAngle == m_nAngle-1) { int iUpperAngle = 0; int iLowerPos = (m_nPos-1) - iFloorPos; int iUpperPos = (m_nPos-1) - (iFloorPos+1); result = static_cast ((1-dAngleFrac) * (1-dPosFrac) * m_ppMatrix[iFloorAngle][iFloorPos] + dAngleFrac * (1-dPosFrac) * m_ppMatrix[iUpperAngle][iLowerPos] + dPosFrac * (1-dAngleFrac) * m_ppMatrix[iFloorAngle][iFloorPos+1] + dAngleFrac * dPosFrac * m_ppMatrix[iUpperAngle][iUpperPos]); } else if (iFloorAngle == -1) { int iLowerAngle = m_nAngle - 1; int iLowerPos = (m_nPos-1) - iFloorPos; int iUpperPos = (m_nPos-1) - (iFloorPos+1); result = static_cast ((1-dAngleFrac) * (1-dPosFrac) * m_ppMatrix[iLowerAngle][iLowerPos] + dAngleFrac * (1-dPosFrac) * m_ppMatrix[iFloorAngle+1][iFloorPos] + dPosFrac * (1-dAngleFrac) * m_ppMatrix[iLowerAngle][iUpperPos] + dAngleFrac * dPosFrac * m_ppMatrix[iFloorAngle+1][iFloorPos+1]); } else result = static_cast ((1-dAngleFrac) * (1-dPosFrac) * m_ppMatrix[iFloorAngle][iFloorPos] + dAngleFrac * (1-dPosFrac) * m_ppMatrix[iFloorAngle+1][iFloorPos] + dPosFrac * (1-dAngleFrac) * m_ppMatrix[iFloorAngle][iFloorPos+1] + dAngleFrac * dPosFrac * m_ppMatrix[iFloorAngle+1][iFloorPos+1]); } return result; } }; template class BicubicPolyInterpolator { private: T** const m_ppMatrix; const unsigned int m_nx; const unsigned int m_ny; public: BicubicPolyInterpolator (T** ppMatrix, unsigned int nx, unsigned int ny) : m_ppMatrix(ppMatrix), m_nx(nx), m_ny(ny) {} T interpolate (double dXPos, double dYPos) { // int iFloorX = static_cast(floor (dXPos)); // int iFloorY = static_cast(floor (dYPos)); // double dXFrac = dXPos - iFloorX; // double dYFrac = dYPos - iFloorY; T result = 0; // Need to add code return result; } }; template class LinearInterpolator { private: T* const m_pX; T* const m_pY; const int m_n; const bool m_bZeroOutsideRange; public: LinearInterpolator (T* pY, unsigned int n, bool bZeroOutside = true) : m_pX(0), m_pY(pY), m_n(n), m_bZeroOutsideRange(bZeroOutside) {} LinearInterpolator (T* pX, T* pY, unsigned int n, bool bZeroOutside = true) : m_pX(pX), m_pY(pY), m_n(n), m_bZeroOutsideRange(bZeroOutside) {} double interpolate (double dX, int* piLastFloor = nullptr) { double result = 0; if (! m_pX) { if (dX == 0) result = m_pY[0]; else if (dX < 0) { if (m_bZeroOutsideRange) result = 0; else result = m_pY[0]; } else if (dX == m_n - 1) result = m_pY[m_n-1]; else if (dX > m_n - 1) { if (m_bZeroOutsideRange) result = 0; else result = m_pY[m_n - 1]; } else { int iFloor = static_cast(floor(dX)); result = m_pY[iFloor] + (m_pY[iFloor+1] - m_pY[iFloor]) * (dX - iFloor); } } else { int iLower = -1; int iUpper = m_n; if (piLastFloor && *piLastFloor >= 0 && m_pX[*piLastFloor] < dX) iLower = *piLastFloor; while (iUpper - iLower > 1) { int iMiddle = (iUpper + iLower) >> 1; if (dX >= m_pX[iMiddle]) iLower = iMiddle; else iUpper = iMiddle; } if (dX == m_pX[0]) result = m_pY[0]; else if (dX < m_pX[0]) { if (m_bZeroOutsideRange) result = 0; else result = m_pY[0]; } else if (dX == m_pX[m_n-1]) result = m_pY[m_n-1]; else if (dX > m_pX[m_n - 1]) { if (m_bZeroOutsideRange) result = 0; else result = m_pY[m_n - 1]; } else { if (iLower < 0 || iLower >= m_n) { sys_error (ERR_SEVERE, "Coordinate out of range [linearInterpolate]"); return 0; } if (piLastFloor) *piLastFloor = iLower; result = m_pY[iLower] + (m_pY[iUpper] - m_pY[iLower]) * ((dX - m_pX[iLower]) / (m_pX[iUpper] - m_pX[iLower])); } } return result; } }; ctsim-7.0.0/include/trace.h0000644000175000017500000000460515253543770014561 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: trace.h ** Purpose: Header file for Trace class ** Author: Kevin Rosenberg ** Date Started: Oct 2000 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef TRACE_H #define TRACE_H #include #include class Trace { public: static const int TRACE_INVALID; static const int TRACE_NONE; static const int TRACE_CONSOLE; static const int TRACE_PHANTOM; static const int TRACE_PROJECTIONS; static const int TRACE_PLOT; static const int TRACE_CLIPPING; static const int BIT_CONSOLE; static const int BIT_PHANTOM; static const int BIT_PROJECTIONS; static const int BIT_PLOT; static const int BIT_CLIPPING; Trace (std::string_view traceString); void addTrace (std::string_view traceString); bool isTrace (std::string_view traceQuery) const; int getTraceLevel(void) const { return m_traceLevel; } static int convertTraceNameToID (std::string_view name); static const char* convertTraceIDToTitle (int idTrace); static std::string_view convertTraceIDToName (int idTrace); static int getTraceCount() {return s_iTraceCount;} static const char** getTraceNameArray() {return s_aszTraceName;} static const char** getTraceTitleArray() {return s_aszTraceTitle;} private: int m_traceLevel; bool addTraceElements (std::string_view traceString); static const char* s_aszTraceName[]; static const char* s_aszTraceTitle[]; static const int s_iTraceCount; }; #endif ctsim-7.0.0/include/ezplot.h0000644000175000017500000002761315253702131014770 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: ezplot.h ** Purpose: Header file for EZplot library ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef __H_EZPLOT #define __H_EZPLOT #include #include #include #include #include #include #include #include "ctsupport.h" #include "sgp.h" #include "pol.h" class EZPlotCurve { public: std::vector x; std::vector y; int m_iPointCount; EZPlotCurve (std::span x, std::span y); ~EZPlotCurve(); }; //---------------------------------------------------------------------- // GLOBAL VARIABLES //---------------------------------------------------------------------- // axis definitions enum { LINEAR = 1, // linear axis LOG, // logrithmic axis NOAXIS, // don't plot axis }; // tick definitions enum { ABOVE = 1, BELOW, RIGHT, LEFT, }; // line types enum { NOLINE = 0, SOLID, DASH, DASH1, DASH2, DASH3, DASH4, }; // symbol definitions enum { SB_CROSS = 1, SB_PLUS, SB_BOX, SB_CIRCLE, SB_ERRORBAR, SB_POINT, MAXSYMBOL, }; enum { INSIDE = 1, // values of o_legendbox OUTSIDE, NOLEGEND, }; struct KeywordCodeTable { const char* keyword; int code; }; /*----------------------------------------------------------------------------- * GLOBAL VARIABLES * * Naming Convention: * i_ Internal variable * Not user changable * o_ Option variable * Normal variable that is user modifiable * These variables must always have a valid value * d_ Device variable * Variables controlling devices * clr_ Color variable * Holds a color value * c_ Character string variable * Contains a character string * v_ Value variable * User modifiable variable associated with the set variable (s_) * These variables do not always have a valid value * These variables change assumption EZPLOT makes about the plot * s_ Set variable. * true if associated value variable (v_) has been set by the user *---------------------------------------------------------------------------*/ #include using EZPlotCurveIterator = std::vector::iterator; using EZPlotCurveConstIterator = std::vector::const_iterator; class SGP; class EZPlot { private: //---------------------------------------------------------------------- // POL Codes //---------------------------------------------------------------------- enum { S_DATA = 2, S_HELP, S_EXIT, S_CURVE, S_SOLID, S_DASH, S_NOLINE, S_BLACK, S_RED, S_GREEN, S_BLUE, S_SYMBOL, S_PEN, S_EVERY, S_NONE, S_LEGEND, S_XLEGEND, S_YLEGEND, S_XLIN, S_YLIN, S_XLOG, S_YLOG, S_XLABEL, S_YLABEL, S_XLENGTH, S_YLENGTH, S_XTICKS, S_YTICKS, S_ABOVE, S_LABEL, S_BELOW, S_NOLABEL, S_RIGHT, S_LEFT, S_XAUTOSCALE, S_YAUTOSCALE, S_XMIN, S_YMIN, S_XMAX, S_YMAX, S_LXFRAC, S_LYFRAC, S_XCROSS, S_YCROSS, S_NOXAXIS, S_NOYAXIS, S_XPORIGIN, S_YPORIGIN, S_TITLE, S_XTITLE, S_YTITLE, S_REPLOT, S_CLEAR, S_STORE, S_RESTORE, S_AMARK, S_NO, S_INTERACTIVE, S_UNITS, S_INCHES, S_USER, S_BOX, S_NOBOX, S_GRID, S_NOGRID, S_MAJOR, S_MINOR, S_COLOR, S_LEGENDBOX, S_TAG, S_TEXTSIZE, }; static const struct KeywordCodeTable m_sKeywords[]; static const int NKEYS; std::vector m_vecCurves; std::vector m_veciColor; std::vector m_vecbColorSet; std::vector m_veciSymbol; std::vector m_vecbSymbolSet; std::vector m_veciSymbolFreq; std::vector m_vecbSymbolFreqSet; std::vector m_veciLinestyle; std::vector m_vecbLinestyleSet; std::vector m_vecsLegend; std::vector m_vecbLegendSet; int getColor (unsigned int iCurve) const; // Whether a colour was asked for, as against the default this curve // would get anyway. A caller that assigns colours itself uses this to // leave an explicit choice alone. int getSymbol (unsigned int iCurve) const; const std::string* getLegend (unsigned int iCurve) const; int getSymbolFreq (unsigned int iCurve) const; int getLinestyle (unsigned int iCurve) const; public: // Public because a caller that adds curves is the only one that knows // which is which: the colour and the name belong with the data. void setColor (unsigned int iCurve, int iColor); bool isColorSet (unsigned int iCurve) const { return m_vecbColorSet.size() > iCurve && m_vecbColorSet[iCurve]; } void setSymbol (unsigned int iCurve, int iSymbol); void setSymbolFreq (unsigned int iCurve, int iSymbolFreq); void setLinestyle (unsigned int iCurve, int iLinestyle); void setLegend (unsigned int iCurve, const std::string& strLegend); void setLegend (unsigned int iCurve, std::string_view legend); private: int m_iCurrentCurve; // Colors int clr_axis; // color of all axis lines int clr_title; // color of main title int clr_label; // color of axis labels int clr_legend; // color of legend box int clr_grid; // color of grid lines int clr_number; // color of axis number labels // Options double o_xporigin, o_yporigin; // origin of plot frame in NDC double o_xlength, o_ylength; // length of plot frame in NDC std::string c_xlabel; // label for x axis std::string c_ylabel; // label for y axis std::string c_title; // title to print above graph int o_linestyle, o_color; // style to use for curves all subsequent curves to EZPLOT int o_xaxis, o_yaxis; // Specifies where axis & labels are drawn bool o_grid; // Flag to draw a grid at major ticks bool o_box; // Flag to draw a box around the graph int o_xticks, o_yticks; // direction to draw tick marks bool o_xtlabel, o_ytlabel; // TRUE if label tick marks int o_xmajortick, o_ymajortick; // number of major ticks to draw int o_xminortick, o_yminortick; // number of minor ticks between major ticks int o_symbol; // Symbol type, (0 = no symbol) int o_symfreq; // frequency to draw symbols at curve points int o_legendbox; // controls whether legend is inside or outside of the axis extents int o_tag; // controls whether to draw tag at end of axes // VALUE & SET variables double v_xmin, v_xmax, v_ymin, v_ymax; // user supplied axis endpoints bool s_xmin, s_xmax, s_ymin, s_ymax; // TRUE is endpoint has been set double v_xtitle, v_ytitle; // NDC position to plot title bool s_xtitle, s_ytitle; // TRUE if set position for title double v_xcross, v_ycross; // position that axes cross bool s_xcross, s_ycross; // TRUE if set axes cross position double v_xlegend, v_ylegend; // upper-left position of legend box in NDC bool s_xlegend, s_ylegend; // TRUE if set position of legend box int v_lxfrac, v_lyfrac; // number of digits to right of decimal place bool s_lxfrac, s_lyfrac; // TRUE if set number of fractional digits double v_textsize; // size of text in NDC bool s_textsize; // TRUE if user set size of text // Global variables double charheight; // Height of characters in NDC double charwidth; // Height of characters in NDC double xp_min, xp_max, yp_min, yp_max; // boundry of plot frame in NDC double xa_min, xa_max, ya_min, ya_max; // extent of axes in NDC double xgw_min, xgw_max, ygw_min, ygw_max; // boundary of graph in input coords double xgn_min, xgn_max, ygn_min, ygn_max; // boundy of graph in NDC double xt_min, xt_max, yt_min, yt_max; // boundary of axis ticks double xl_min, xl_max, yl_min, yl_max; // boundary of legend box double title_row; // y-coord of title row double xtl_ofs; // Offset y-coord of x tick labels from axis double ytl_ofs; // Offset x-coord of y tick labels from axis double xlbl_row; // row of x label in world coord double ylbl_col; // column of y label in world coord double xw_tickinc, yw_tickinc; // increment between major ticks in WC double xn_tickinc, yn_tickinc; // increment between major ticks in NDC int x_nint, y_nint; // number of intervals along x & y axes int x_fldwid, x_frac; // numeric field sizes & number of digits int y_fldwid, y_frac; // in fraction of number, used for printf() double xtl_wid, ytl_wid; // length of ticks labels in NDC double tl_height; // height of tick labels in NDC // The printf format for the tick labels, built by make_numfmt(). These // were char[20], written with sprintf: a wide enough axis range makes a // format string longer than that, and nothing bounded it. std::string x_numfmt; std::string y_numfmt; double m_dVP_xmin, m_dVP_ymin; double m_dVP_xmax, m_dVP_ymax; double m_dVP_xscale, m_dVP_yscale; double m_xWorldScale, m_yWorldScale; void drawAxes(); void symbol (int sym, double symwidth, double symheight); std::string make_numfmt ( int *fldwid, int *nfrac, double min, double max, int nint); int axis_scale (double min, double max, int nint, double *minp, double *maxp, int *nintp); SGP* m_pSGP; POL m_pol; void clearCurves (); bool ezcmd (std::string_view comm); bool do_cmd(int lx); void bad_option (std::string_view opt); void initPlotSettings(); void initKeywords (); double convertWorldToNDC_X (double x) { return xgn_min + (x - xgw_min) * m_xWorldScale; } double convertWorldToNDC_Y (double y) { return ygn_min + (y - ygw_min) * m_yWorldScale; } public: EZPlot (); ~EZPlot (); bool ezset (std::string_view command); // Spans rather than a pointer and a count: the two axes have to be the // same length, and with three separate arguments nothing said so or // checked it. A std::vector converts on its own; a raw pointer that is // all a caller has says its length as {p, n}. void addCurve (std::span x, std::span y); void addCurve (std::span x, std::span y); void addCurve (std::span x, std::span y); void addCurve (std::span y); void addCurve (std::span y); // How many curves have been added. A curve whose axes did not agree is // refused, so this is how a caller or a test can tell. std::size_t curveCount() const { return m_vecCurves.size(); } void plot (SGP* pSGP); }; #endif ctsim-7.0.0/include/procsignal.h0000644000175000017500000001674415253543770015633 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: filter.h ** Purpose: Signal filter header file ** Programmer: Kevin Rosenberg ** Date Started: June 2000 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef PROCSIGNAL_H #define PROCSIGNAL_H #include #include #include #include #ifdef HAVE_CONFIG_H #include "config.h" #endif #ifdef HAVE_FFTW #include #endif #include class SignalFilter; class SGP; using CTSimComplex = std::complex; class ProcessSignal { public: static const int FILTER_METHOD_INVALID; static const int FILTER_METHOD_CONVOLUTION; static const int FILTER_METHOD_FOURIER; static const int FILTER_METHOD_FOURIER_TABLE; static const int FILTER_METHOD_FFT; #if HAVE_FFTW static const int FILTER_METHOD_FFTW; static const int FILTER_METHOD_RFFTW; #endif static const int FILTER_GENERATION_INVALID; static const int FILTER_GENERATION_DIRECT; static const int FILTER_GENERATION_INVERSE_FOURIER; enum { FORWARD = -1, BACKWARD = 1, }; ProcessSignal (std::string_view svFilterName, std::string_view svFilterMethodName,double bw, double signalIncrement, int n, double param, std::string_view svDomainName, std::string_view svFilterGenerationName, const int zeropad, const int preinterpolationFactor, const int iTraceLevel, int iGeometry, double dFocalLength, double dSourceDetectorLength, SGP* pSGP = nullptr); ~ProcessSignal(); // Spans: the input has to hold nSignalPoints values and the output // room for as many, and with two bare pointers neither could be said // or checked. void filterSignal (std::span input, std::span output) const; bool fail(void) const {return m_fail;} const std::string& failMessage(void) const {return m_failMessage;} void setTraceLevel (int traceLevel) {m_traceLevel = traceLevel; } int getNFilterPoints (void) const { return m_nFilterPoints; } double getFilterMin(void) const {return m_dFilterMin;} double getFilterMax(void) const {return m_dFilterMax;} double getFilterIncrement(void) const {return m_dFilterInc;} // Spans, so the filter travels with its length. std::span getFilter (void) { return m_vecFilter; } std::span getFilter (void) const { return m_vecFilter; } int idFilterGeneration() const { return m_idFilterGeneration;} static int getFilterGenerationCount() {return s_iFilterGenerationCount;} static const char* const* getFilterGenerationNameArray() {return s_aszFilterGenerationName;} static const char* const* getFilterGenerationTitleArray() {return s_aszFilterGenerationTitle;} static int convertFilterGenerationNameToID (std::string_view name); static std::string_view convertFilterGenerationIDToName (const int idFG); static const char* convertFilterGenerationIDToTitle (const int idFG); static int getFilterMethodCount() {return s_iFilterMethodCount;} static const char* const* getFilterMethodNameArray() {return s_aszFilterMethodName;} static const char* const* getFilterMethodTitleArray() {return s_aszFilterMethodTitle;} static int convertFilterMethodNameToID (std::string_view name); static std::string_view convertFilterMethodIDToName (const int idFilterMethod); static const char* convertFilterMethodIDToTitle (const int idFilterMethod); // transforms using direct trigometric calculation static void finiteFourierTransform (const double input[], double output[], const int n, const int direction); static void finiteFourierTransform (const double input[], std::complex output[], const int n, const int direction); static void finiteFourierTransform (const std::complex input[], std::complex output[], const int n, const int direction); static void finiteFourierTransform (const std::complex input[], double output[], const int n, const int direction); static int addZeropadFactor (int n, int iZeropad); private: std::string m_nameFilterMethod; std::string m_nameFilterGeneration; int m_idFilterMethod; int m_idFilterGeneration; int m_nSignalPoints; std::vector m_vecFourierCosTable; std::vector m_vecFourierSinTable; int m_nFilterPoints; double m_dSignalInc; double m_dFilterInc; double m_dFilterMin; double m_dFilterMax; std::vector m_vecFilter; bool m_bFrequencyFiltering; // Variables also kept in SignalFilter class int m_idFilter; int m_idDomain; int m_traceLevel; double m_dBandwidth; double m_dFilterParam; int m_iZeropad; int m_nOutputPoints; int m_iPreinterpolationFactor; int m_idGeometry; double m_dFocalLength; double m_dSourceDetectorLength; bool m_fail; std::string m_failMessage; static const char* const s_aszFilterMethodName[]; static const char* const s_aszFilterMethodTitle[]; static const int s_iFilterMethodCount; static const char* const s_aszFilterGenerationName[]; static const char* const s_aszFilterGenerationTitle[]; static const int s_iFilterGenerationCount; #ifdef HAVE_FFTW double *m_adRealFftInput, *m_adRealFftOutput, *m_adRealFftSignal, *m_adRealFftBackwardOutput; fftw_plan m_realPlanForward, m_realPlanBackward; fftw_complex *m_adComplexFftInput, *m_adComplexFftOutput, *m_adComplexFftSignal, *m_adComplexFftBackwardOutput; fftw_plan m_complexPlanForward, m_complexPlanBackward; #endif void init (const int idFilter, int idFilterMethod, double dBandwidth, double dSignalIncrement, int nSignalPoints, double dFilterParam, const int idDomain, int idFilterGeneration, const int iZeropad, const int iPreinterpolationFactor, const int iTraceLevel, const int iGeometry, double dFocalLength, double dSourceDetectorLength, SGP* pSGP); // transforms that use precalculated trig tables, therefore don't // require number of data points (n) as an argument void finiteFourierTransform (const double input[], std::complex output[], const int direction) const; void finiteFourierTransform (const std::complex input[], std::complex output[], const int direction) const; void finiteFourierTransform (const std::complex input[], double output[], const int direction) const; double convolve (const double func[], const double filter[], const double dx, const int n, const int np) const; double convolve (const double f[], const double dx, const int n, const int np) const; double convolve (const float f[], const double dx, const int n, const int np) const; }; #endif ctsim-7.0.0/include/ctsupport_hooks.h0000644000175000017500000000550715253666241016732 0ustar kevinkevin/***************************************************************************** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef CTSUPPORT_HOOKS_H #define CTSUPPORT_HOOKS_H // the libraries no longer know anything about the graphical program. // Until 6.3.x, libctsupport/syserror.cpp and globalvars.cpp included // ../src/ctsim.h and posted wxWidgets events, and libctsim/procsignal.cpp // opened an EZPlotDialog. A library reaching up into whichever GUI happens to // be linked is the single biggest obstacle to replacing that GUI, so those // points are now callbacks that an application may install. Nothing is // installed by default: the command-line tools simply get the console // behaviour, and a GUI installs handlers at start-up. #include #include #include // Diagnostic sink. iSeverity is one of the ERR_* constants in ctsupport.h. // The default writes to stdout, which is what the command-line tools have always done. // A handler must be safe to call from worker threads. using SysErrorHandler = std::function; void sysErrorSetHandler (SysErrorHandler handler); // The handler in force, so that a caller can put one of its own in place // for a while and then restore what was there. SysErrorHandler sysErrorGetHandler (); void sysErrorClearHandler (); bool sysErrorHasHandler (); // Optional plot sink used by the signal-processing trace levels. The // default is to do nothing; a GUI can display the curve. An empty x says // the points are plotted against their index; there is no null span. using TracePlotHandler = std::function x, std::span y)>; void traceSetPlotHandler (TracePlotHandler handler); void traceClearPlotHandler (); bool traceHasPlotHandler (); void tracePlot (const std::string& strTitle, std::span x, std::span y); #endif ctsim-7.0.0/include/timer.h0000644000175000017500000000516215253666241014601 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: timer.h ** Purpose: Header file for Timer class ** Author: Kevin Rosenberg ** Date Started: Sep 2000 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef _TIMER_H #define _TIMER_H #include #include #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include // timing uses std::chrono::steady_clock. It is monotonic, so a change // to the wall clock cannot make an elapsed time jump or go negative, and its // resolution is that of the system clock rather than the microsecond ticks of // gettimeofday. Note that std::clock() would be the wrong choice here: it // reports CPU time, so under OpenMP it returns roughly thread-count times the // real elapsed time, which is not what a recorded calculation time should be. class Timer { public: Timer (void) : m_timeStart (std::chrono::steady_clock::now()), m_timeElapsed (0.) {} virtual ~Timer (void) {} virtual double timerEnd (void) { const std::chrono::duration elapsed = std::chrono::steady_clock::now() - m_timeStart; m_timeElapsed = elapsed.count(); return (m_timeElapsed); } virtual void timerReport (std::string_view msg) const { std::cout << msg << ": " << m_timeElapsed << " seconds" << std::endl; } virtual double timerEndAndReport (std::string_view msg) { double t = timerEnd (); timerReport (msg); return (t); } double getTimeElapsed (void) const { return m_timeElapsed; } protected: std::chrono::steady_clock::time_point m_timeStart; double m_timeElapsed; }; #endif // _TIMER_H ctsim-7.0.0/include/ctsupport.h0000644000175000017500000002553015253543770015526 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** File Name: ctsupport.h ** Author: Kevin Rosenberg ** Purpose: Header file for CT support library ** Date Started: Dec. 83 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef CTSUPPORT_H #define CTSUPPORT_H #ifdef HAVE_CONFIG_H #include "config.h" #endif #ifdef MSVC #endif #define STR_SIZE STR_MAX_LEN+1 #include #include #include #include #include #include #include #include #include #include #include #include #include // TRUE, FALSE and OK were macros here until 7.0.0. They are gone: C++ has // true and false, and the macros collided with other libraries' identifiers. // OK in particular broke every DCMTK build whose OFPair is the class template // rather than std::pair, because its template parameter is named OK -- the // macro turned "template" into "typename 1". /*----------------------------------------------------------------------*/ #define SHOW(var, fmt) { cerr << "var = " << var << endl; } /*----------------------------------------------------------------------*/ #define NEWLINE '\n' #define TAB '\t' #define EOS '\0' #define BLANK ' ' /*----------------------------------------------------------------------*/ constexpr int ERR_TRACE = -1; constexpr int ERR_WARNING = 0; constexpr int ERR_SEVERE = 1; constexpr int ERR_FATAL = 2; /*----------------------------------------------------------------------*/ /* codes for open command */ #ifdef MSVC #else #endif /*----------------------------------------------------------------------*/ #if !defined(O_BINARY) && !defined(MSVC) #define O_BINARY (0) #endif #ifndef S_IWRITE #define S_IWRITE S_IWUSR #endif /*----------------------------------------------------------------------*/ // The sized names the file formats are written in. They were chosen // from the sizes of int, long and short by the configure step, which // gave the wrong answer wherever configure had not run and could not know // that long is 32 bits on Windows even on a 64-bit machine. // knows. using kint8 = std::int8_t; using kuint8 = std::uint8_t; using kint16 = std::int16_t; using kuint16 = std::uint16_t; using kint32 = std::int32_t; using kuint32 = std::uint32_t; using kint64 = std::int64_t; using kuint64 = std::uint64_t; using kfloat32 = float; using kfloat64 = double; static_assert (sizeof (kfloat32) == 4, "float is not 32 bits on this platform"); static_assert (sizeof (kfloat64) == 8, "double is not 64 bits on this platform"); // The part of a path after the last separator. // // std::filesystem rather than a search for '/': on Windows a backslash // separates too, and on Unix a backslash is an ordinary character in a // name. The library knows which applies where; a hand-written search // cannot be right on both. It returns a string rather than a view into // the argument, because the path it builds is its own. inline std::string fileBasename (std::string_view filename) { return std::filesystem::path (filename).filename().string(); } // The directory a path names, with no trailing separator; empty when the // path has no directory part. inline std::string fileDirname (std::string_view filename) { return std::filesystem::path (filename).parent_path().string(); } // A path resolved against a directory: strFile as given when it is // absolute, and joined to strDirectory when it is not. inline std::string filePathRelativeTo (std::string_view strDirectory, std::string_view strFile) { const std::filesystem::path p (strFile); if (p.is_absolute() || strDirectory.empty()) return p.string(); return (std::filesystem::path (strDirectory) / p).string(); } /* strfuncs.cpp */ // The text after any leading run of the given characters. str_lower, // str_upper, str_rm_tail and str_wrm_tail sat beside this and had no // callers at all; they modified their argument in place and are gone. std::string_view str_skip_head (std::string_view str, std::string_view charlist); /* syserror.cpp */ // The message as it stands: for a message with nothing to format, and for // one assembled at run time. A message built at run time can only come // this way, which is the point of having two of these -- it can never be // taken for a format string and read arguments that were never passed. // Equality ignoring case, ASCII, for matching a name against a table of // names. The standard library has no case-insensitive compare, and // strcasecmp is not standard C++; this is what the convert*NameToID // functions use. bool equalsIgnoringCase (std::string_view a, std::string_view b); void sys_error (int severity, std::string_view message); // A message with values in it. The format is std::format's: "{}" for a // value, "{:6.2f}" for one with a width and a precision. It has to be a // constant expression, and it is checked against the arguments as the // program is built, so a mismatch is a compile error rather than something // that goes wrong when the message is finally printed. template void sys_error (int severity, std::format_string fmt, Args&&... args) { sys_error (severity, std::string_view (std::format (fmt, std::forward (args)...))); } // Decorates a message with its severity and applies the error-count limit, // as sys_error does, without dispatching it. Used by the few places that // want the text. void sys_verror (std::string& strOutput, int severity, std::string_view message); void sys_error_level(int severity); extern unsigned long int g_lSysErrorMaxCount; // Math Section #include constexpr double PI = 3.14159265358979323846; constexpr double HALFPI = 1.57079632679489661923; constexpr double TWOPI = 6.28318530717958647692; constexpr double SQRT2 = 1.414213562373095049; constexpr double F_EPSILON = 1.0E-6; constexpr double D_EPSILON = 1.0E-10; using GRFMTX_2D = double[3][3]; using GRFMTX_3D = double[4][4]; inline double convertDegreesToRadians (double x) { return (x * (PI/180.)); } inline double convertRadiansToDegrees (double x) { return (x*(180./PI)); } template inline T nearest (double x) { return (x > 0 ? static_cast(x+0.5) : static_cast(x-0.5)); } template inline T maxValue (T x, T y) { return (x > y ? x : y); } inline bool isEven (int n) { return (n % 2) == 0; } inline bool isOdd (int n) { return (n % 2) != 0; } #if 0 inline bool isEven (long n) { return (n % 2) == 0; } inline bool isOdd (long n) { return (n % 2) != 0; } #endif inline int imax (int a, int b) { return (a >= b ? a : b); } inline double dmax (double a, double b) { return (a >= b ? a : b); } template inline T clamp (T value, T lowerBounds, T upperBounds) { return (value >= upperBounds ? upperBounds : (value <= lowerBounds ? lowerBounds : value )); } template inline T lineLength (T x1, T y1, T x2, T y2) { return static_cast( sqrt ((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1)) ); } template inline void minmax_array (const T* array, const int n, T& min, T& max) { max = min = array[0]; for (int i = 1; i < n; i++) if (array[i] < min) min = array[i]; else if (array[i] > max) max = array[i]; } ////////////////////////////////////////////////////////////// // FUNTION DECLARATIONS ////////////////////////////////////////////////////////////// // clip.cpp bool clip_rect (double& x1, double& y1, double& x2, double& y2, const double rect[4]); bool clip_segment (double& x1, double& y1, double& x2, double& y2, const double u, const double v); bool clip_sector (double& x1, double& y1, double& x2, double& y2, const double u, const double v); bool clip_circle (double& x1, double& y1, double& x2, double& y2, const double cx, const double cy, const double radius, double t1, double t2); bool clip_triangle (double& x1, double& y1, double& x2, double& y2, const double u, const double v, const int clip_xaxis); // xform.cpp void indent_mtx2 (GRFMTX_2D m); void xlat_mtx2 (GRFMTX_2D m, const double x, const double y); void scale_mtx2 (GRFMTX_2D m, const double sx, const double sy); void rot_mtx2 (GRFMTX_2D m, const double theta); void mult_mtx2 (const GRFMTX_2D m1, const GRFMTX_2D m2, GRFMTX_2D result); void xform_mtx2 (const GRFMTX_2D m, double& x, double& y); void copy_mtx2 (GRFMTX_2D to, const GRFMTX_2D from); void rotate2d (double x[], double y[], int pts, double angle); void xlat2d (double x[], double y[], int pts, double xoffset, double yoffset); void scale2d (double x[], double y[], int pts, double xfact, double yfact); // mathfuncs.cpp double normalizeAngle (double theta); double integrateSimpson (const double xmin, const double xmax, const double *y, const int np); void vectorNumericStatistics (std::vector vec, const int nPoints, double& min, double& max, double& mean, double& mode, double& median, double& stddev); /*----------------------------------------------------------------------*/ /* screen character codes */ /* audio.cpp */ void cio_beep(void); void cio_tone(double freq, double length); /* crtput.cpp */ void cio_put_c(int c); void cio_put_cc(int c, int count); void cio_put_str(const char *str); /* kbget.cpp */ unsigned int cio_kb_getc(void); void cio_kb_ungetc(unsigned int c); char *cio_kb_gets(char *str, int maxlen); unsigned int cio_kb_waitc(const char *astr, int beep); // Keyboard Section // ASCII Section // #define LF 0x0A // #define CR 0x0D constexpr char DQUOTE = '"'; constexpr char BSLASH = '\\'; constexpr char SHARP = '#'; #define LBRACE '{' #define RBRACE '}' #define LPAREN '(' #define RPAREN ')' #define LBRACK '[' #define RBRACK ']' #define LANBRACK '<' #define RANBRACK '>' #define SEMICOL ';' #define UNDERLIN '_' #define COMMA ',' #define CARET '^' #define TILDE '~' #define ATSIGN '@' #define AMPERSAND '&' #define EXCLAM '!' #define DOLLAR '$' #define PERCENT '%' #define PLUS '+' #define HYPHEN '-' #define EQUALS '=' #define QUESTION '?' #define PERIOD '.' #define VERTBAR '|' #endif /* #ifndef CTSUPPORT_H */ ctsim-7.0.0/include/projections.h0000644000175000017500000002535615253666241016027 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: projections.h ** Purpose: Header file for Projections class ** Programmer: Kevin Rosenberg ** Date Started: July 1, 1984 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifndef PROJECTIONS_H #define PROJECTIONS_H #include #include #include class Scanner; class DetectorArray; class Array2dFileLabel; class fnetorderstream; #include "array2dfile.h" #include "imagefile.h" #include #include //used for rebinning divergent beam projections to parallel class ParallelRaysumCoordinate { public: double m_dT; // Distance from center of origin double m_dTheta; // perpendicular angle to origin double m_dRaysum; static bool compareByTheta (ParallelRaysumCoordinate* a, ParallelRaysumCoordinate* b) { return a->m_dTheta == b->m_dTheta ? b->m_dT > a->m_dT : b->m_dTheta > a->m_dTheta; } // sort first by T, then Theta static bool compareByT (ParallelRaysumCoordinate* a, ParallelRaysumCoordinate* b) { return a->m_dT == b->m_dT ? b->m_dTheta > a->m_dTheta : b->m_dT > a->m_dT; } }; class ParallelRaysums { public: enum { THETA_RANGE_UNCONSTRAINED = 0, THETA_RANGE_NORMALIZE_TO_TWOPI, THETA_RANGE_FOLD_TO_PI, }; ParallelRaysums (const Projections* pProjections, int iThetaRange); ~ParallelRaysums (); using CoordinateContainer = std::vector; using CoordinateIterator = CoordinateContainer::iterator; CoordinateContainer& getCoordinates() {return m_vecpCoordinates;} int getNumCoordinates() const {return m_iNumCoordinates;} void getLimits (double* dMinT, double* dMaxT, double* dMinTheta, double* dMaxTheta) const; CoordinateContainer& getSortedByT(); CoordinateContainer& getSortedByTheta(); double interpolate (double* pdX, double* pdY, int n, double dXValue, int* iLastFloor = nullptr); void getThetaAndRaysumsForT (int iT, double* pdTheta, double* pdRaysum); void getDetPositions (double* pdDetPos); private: CoordinateContainer m_vecpCoordinates; CoordinateContainer m_vecpSortedByT; CoordinateContainer m_vecpSortedByTheta; std::vector m_vecCoordinates; int m_iNumCoordinates; int m_iNumView; int m_iNumDet; int m_iThetaRange; }; // Projections class Projections { public: static const int POLAR_INTERP_INVALID; static const int POLAR_INTERP_NEAREST; static const int POLAR_INTERP_BILINEAR; static const int POLAR_INTERP_BICUBIC; Projections (const int nView, const int nDet); Projections (const Scanner& scanner); Projections (); ~Projections (); static int getInterpCount() {return s_iInterpCount;} static const char* const* getInterpNameArray() {return s_aszInterpName;} static const char* const* getInterpTitleArray() {return s_aszInterpTitle;} static int convertInterpNameToID (std::string_view name); static const char* convertInterpIDToName (const int interpID); static const char* convertInterpIDToTitle (const int interpID); void initFromScanner (const Scanner& scanner); bool initFromSomatomAR_STAR (int iNViews, int iNDets, unsigned char* pData, unsigned long lDataLength); void printProjectionData (int startView, int endView); void printProjectionData (); void printScanInfo (std::ostringstream& os) const; int Helical180LI(int interpView); int Helical180LI_Equiangular(int interpView); int HalfScanFeather(void); bool read (const std::string& fname); bool read (const char* fname); // HDF5 storage; read()/write() dispatch to these automatically. // structured history and extensible metadata, as for images. void addHistory (std::string_view operation, const HistoryParams& params, double dCalcTime = 0.); void setUserAttribute (std::string_view name, std::string_view value); void setUserAttribute (std::string_view name, double dValue); bool getUserAttribute (std::string_view name, std::string& strValue) const; const std::map>& userAttributes() const { return m_mapUser; } bool readHDF5 (std::string_view filename); bool writeHDF5 (std::string_view filename); bool write (const char* fname); bool write (const std::string& fname); bool detarrayRead (fnetorderstream& fs, DetectorArray& darray, const int view_num); bool detarrayWrite (fnetorderstream& fs, const DetectorArray& darray, const int view_num); Projections* interpolateToParallel() const; bool convertPolar (ImageFile& rIF, int iInterpolation); // Direct Fourier reconstruction: the whole sequence (central-slice transform, // polar-to-Cartesian interpolation, inverse 2-D transform and scaling) in one // place, so the GUI and the command line cannot disagree about normalization. bool reconstructFourier (ImageFile& rIF, int iInterpolation, int iZeropad); // gridding (non-uniform FFT) reconstruction. Instead of gathering a // value for each Cartesian frequency by interpolating between polar // samples, each polar sample is convolved onto the Cartesian grid with a // Kaiser-Bessel kernel, optionally weighted by the polar sampling // density, and the result is deapodized after the inverse transform. // dOversample is the grid oversampling factor (Beatty recommends 1.5 to // 2), iKernelWidth the kernel width in grid samples (4 is the usual // choice). // // Density compensation is always applied: without the |nu| weighting the // polar samples crowd the low frequencies and the result is not // normalized at all. bool reconstructFourierGrid (ImageFile& rIF, int iZeropad, double dOversample, int iKernelWidth); bool convertFFTPolar (ImageFile& rIF, int iInterpolation, int iZeropad); // Maps each pixel of a *frequency-domain* image onto (view, detector-sample) // coordinates of the transformed projections. Distinct from the spatial // mapping below: the radial axis is a frequency, not a detector position. void calcArrayFrequencyCoordinates (unsigned int nx, unsigned int ny, double** ppdView, double** ppdDet, int iNumDetWithZeros, double dDeltaT, double dFOV) const; void calcArrayPolarCoordinates (unsigned int nx, unsigned int ny, double** ppdView, double** ppdDet, int iNumDetWithZeros, double dZeropadRatio, double dDetInc); void interpolatePolar (ImageFileArray& v, ImageFileArray& vImag, unsigned int nx, unsigned int ny, std::complex** ppcDetValue, double** ppdDet, double** ppdView, unsigned int nView, unsigned int nDet, unsigned int nDetWithZeros, int iInterpolate); void setNView (int nView); // used in MPI to restrict # of views void setRotInc (double rotInc) { m_rotInc = rotInc;} void setDetInc (double detInc) { m_detInc = detInc;} void setCalcTime (double calcTime) {m_calcTime = calcTime;} void setRemark (const char* remark) {m_remark = remark; m_label.setLabelString(remark);} void setRemark (const std::string& remark) {setRemark(remark.c_str());} double detStart() const {return m_detStart;} double rotStart() const {return m_rotStart;} double calcTime() const {return m_calcTime;} double viewLen() const {return m_dViewDiameter / SQRT2;} const char* remark() const {return m_remark.c_str();} double detInc() const {return m_detInc;} double rotInc() const {return m_rotInc;} int nDet() const {return m_nDet;} int nView() const {return m_nView;} int geometry() const {return m_geometry;} double focalLength() const {return m_dFocalLength;} double sourceDetectorLength() const { return m_dSourceDetectorLength;} double viewDiameter() const {return m_dViewDiameter; } double fanBeamAngle() const {return m_dFanBeamAngle;} // for ctdump double phmLen() const { return m_dViewDiameter / SQRT2; } void setPhmLen(double phmLen) { m_dViewDiameter = phmLen * SQRT2; } const std::string& getFilename() const {return m_filename;} Array2dFileLabel& getLabel() {return m_label;} const Array2dFileLabel& getLabel() const {return m_label;} DetectorArray& getDetectorArray (const int iview) { return (*m_projData[iview]); } const DetectorArray& getDetectorArray (const int iview) const { return (*m_projData[iview]); } static bool copyHeader (std::string_view filename, std::ostream& os); static bool copyViewData (std::string_view filename, std::ostream& os, int startView, int endView); private: int m_headerSize; // Size of disk file header int m_geometry; // Geometry of scanner class DetectorArray **m_projData; // Pointer to array of detarray_st pointers std::string m_remark; // description of raysum data int m_nDet; // number of detectors in array int m_nView; // number of rotated views double m_calcTime; // time required to calculate raysums double m_rotStart; // starting view rotation double m_rotInc; // angle between rotations double m_detStart; // distance of beginning detector to center phantom double m_detInc; // increment between detectors double m_dFocalLength; double m_dSourceDetectorLength; double m_dViewDiameter; double m_dFanBeamAngle; kuint32 m_year; // Creation date & time kuint32 m_month; kuint32 m_day; kuint32 m_hour; kuint32 m_minute; kuint32 m_second; std::string m_filename; Array2dFileLabel m_label; std::map> m_mapUser; const static kuint16 m_signature; static const char* const s_aszInterpName[]; static const char* const s_aszInterpTitle[]; static const int s_iInterpCount; bool headerWrite (fnetorderstream& fs); bool headerRead (fnetorderstream& fs); void newProjData (); void deleteProjData (); void init (const int nView, const int nDet); // prevent default methods Projections& operator= (const Projections& rhs); // assignment Projections(const Projections& rhs); // copy }; #endif ctsim-7.0.0/libctsim/0000755000175000017500000000000015253765647013501 5ustar kevinkevinctsim-7.0.0/libctsim/plotfile.cpp0000644000175000017500000003110515253765647016023 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: plotfile.cpp ** Purpose: plotfile class ** Programmer: Kevin Rosenberg ** Date Started: Dec 2000 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #include "ct.h" #include "fileformat.h" #include /////////////////////////////////////////////////////////////////////////// // CLASS IMPLEMENTATION // // Name: PlotFile // Purpose: Plot File storage /////////////////////////////////////////////////////////////////////////// PlotFile::PlotFile (int nCurves, int nRecords) { initHeaders (); setCurveSize (nCurves, nRecords); } PlotFile::PlotFile () { initHeaders (); } PlotFile::~PlotFile () { } void PlotFile::initHeaders () { m_iNumColumns = 0; m_iNumRecords = 0; m_bScatterPlot = false; time_t currentTime = time (nullptr); m_strDate = ctime (¤tTime); m_vecStrDescriptions.clear(); m_vecStrEzsetCommands.clear(); // Neither the y range nor the history is set until something sets it, and // a legacy file, which has neither, must read back with neither. m_bHasYRange = false; m_dYMin = m_dYMax = 0.; m_vecLabels.clear(); } void PlotFile::setCurveSize (int nCols, int nRecords, bool bScatterPlot) { m_iNumColumns = nCols; m_iNumRecords = nRecords; m_bScatterPlot = bScatterPlot; // Sized, not reserved: the columns are stored by index, and an index is // only valid below size() however much capacity lies beyond it. m_vecCurves.assign (static_cast (m_iNumColumns) * m_iNumRecords, 0.); } // Storage format // a Column's records are stored sequentially. It begins at iCol * m_iNumRecords bool PlotFile::addColumn (int iCol, std::span column) { if (iCol < 0 || iCol >= m_iNumColumns) { sys_error (ERR_SEVERE, "Illegal column number {} [PlotFile::addColumn]", iCol); return (false); } // The length the plot expects against the length the caller has. With a // bare pointer this could not be asked, and a short column was read past // its end. if (static_cast (column.size()) < m_iNumRecords) { sys_error (ERR_SEVERE, "Column {} has {} values, {} needed [PlotFile::addColumn]", iCol, column.size(), m_iNumRecords); return (false); } for (int iRec = 0; iRec < m_iNumRecords; iRec++) { m_vecCurves[ iRec + (iCol * m_iNumRecords) ] = column[iRec]; #if 0 sys_error (ERR_TRACE, "Storing m_vecCurves[{}] = {}", iRec + (iCol * m_iNumRecords), column[iRec]); #endif } return true; } bool PlotFile::addColumn (int iCol, std::span column) { if (iCol < 0 || iCol >= m_iNumColumns) { sys_error (ERR_SEVERE, "Illegal column number {} [PlotFile::addColumn]", iCol); return (false); } // The length the plot expects against the length the caller has. With a // bare pointer this could not be asked, and a short column was read past // its end. if (static_cast (column.size()) < m_iNumRecords) { sys_error (ERR_SEVERE, "Column {} has {} values, {} needed [PlotFile::addColumn]", iCol, column.size(), m_iNumRecords); return (false); } for (int iRec = 0; iRec < m_iNumRecords; iRec++) m_vecCurves[ iRec + (iCol * m_iNumRecords) ] = column[iRec]; return true; } void PlotFile::getColumn (int iCol, double* pdColData) const { if (iCol < 0 || iCol >= m_iNumColumns) { sys_error (ERR_SEVERE, "Illegal column number {} [PlotFile::addColumn]", iCol); return; } for (int iRec = 0; iRec < m_iNumRecords; iRec++) pdColData[iRec] = m_vecCurves[ iRec + (iCol * m_iNumRecords) ]; } bool PlotFile::getMinMax (int iStartingCol, double& dMin, double& dMax) const { if (iStartingCol >= m_iNumColumns) { sys_error (ERR_WARNING, "iStartingCol >= iNumColumns"); return false; } int iOffset = iStartingCol * m_iNumRecords; dMin = m_vecCurves[ 0 + iOffset ]; dMax = dMin; for (int iCol = iStartingCol; iCol < m_iNumColumns; iCol++) { int iOffset = iCol * m_iNumRecords; for (int iRec = 0; iRec < m_iNumRecords; iRec++) { double dVal = m_vecCurves[ iRec + iOffset ]; if (dVal < dMin) dMin = dVal; else if (dVal > dMax) dMax = dVal; } } // sys_error (ERR_TRACE, "dMin={}, dMax={}", dMin, dMax); return true; } bool PlotFile::statistics (int iStartingCol, double& min, double& max, double& mean, double& mode, double& median, double &stddev) const { if (iStartingCol >= m_iNumColumns) { sys_error (ERR_WARNING, "iStartingCol >= iNumColumns"); return false; } int iNPoints = (m_iNumColumns - iStartingCol) * m_iNumRecords; std::vector vec; vec.resize (iNPoints); int iVec = 0; for (int iCol = iStartingCol; iCol < m_iNumColumns; iCol++) { int iOffset = iCol * m_iNumRecords; for (int iRec = 0; iRec < m_iNumRecords; iRec++) vec[iVec++] = m_vecCurves[ iRec + iOffset ]; } vectorNumericStatistics (vec, iNPoints, min, max, mean, mode, median, stddev); return true; } bool PlotFile::fileWrite (std::string_view filename) { if (ctsimDefaultFileFormat() == CTSIM_FORMAT_HDF5) return writeHDF5 (filename); return fileWriteText (filename); } // The original text format. It has no place for the y range or the history, // so those are not written; everything else round-trips. bool PlotFile::fileWriteText (std::string_view filename) { m_strFilename = filename; fstream fs (m_strFilename, std::ios::out | std::ios::trunc); if (fs.fail()) { sys_error (ERR_WARNING, "Error opening file {} for writing [fileCreate]", m_strFilename); return false; } if (! headerWrite(fs) || ! columnsWrite (fs)) return false; return true; } void PlotFile::addHistory (std::string_view operation, const HistoryParams& params, double dCalcTime) { Array2dFileLabel label; label.setLabelType (Array2dFileLabel::L_HISTORY); label.setOperation (operation); label.setParameters (params); label.setCalcTime (dCalcTime); #ifdef VERSION label.setVersion (VERSION); #endif std::string strText (operation); if (! params.empty()) strText += ": " + params.toString(); label.setLabelString (strText); m_vecLabels.push_back (label); } void PlotFile::labelsCopyFrom (const Array2dFile& rFile) { for (unsigned int i = 0; i < rFile.nLabels(); i++) m_vecLabels.push_back (rFile.labelGet (i)); } void PlotFile::labelsCopyFrom (const Array2dFileLabel& rLabel) { if (! rLabel.getLabelString().empty() || ! rLabel.getOperation().empty()) m_vecLabels.push_back (rLabel); } bool PlotFile::fileRead (std::string_view filename) { const std::string strFilename (filename); if (ctsimFileIsHDF5 (strFilename.c_str())) return readHDF5 (filename); m_strFilename = filename; #ifdef MSVC fstream fs (m_strFilename, std::ios::in); #else fstream fs (m_strFilename, std::ios::in); // | std::ios::nocreate); #endif if (fs.fail()) { sys_error (ERR_WARNING, "Unable to open file {} [fileRead]", m_strFilename); return false; } if (! headerRead(fs)) return false; setCurveSize (m_iNumColumns, m_iNumRecords); if (! columnsRead(fs)) return false;; return true; } bool PlotFile::headerRead (std::iostream& fs) { if (! fs) { sys_error (ERR_WARNING, "Tried to read header with file closed [headerRead]"); return false; } initHeaders(); fs.seekg (0); bool bFinishedHeaders = false; // Optional opening tag (written from 7.0.0); older files start with the // column and record counts directly. { const std::streampos posStart = fs.tellg(); std::string strFirst; fs >> strFirst; if (strFirst != "") { fs.clear(); fs.seekg (posStart); } } fs >> m_iNumColumns; fs >> m_iNumRecords; if (fs.fail() || m_iNumColumns == 0 || m_iNumRecords == 0) return false; while (! bFinishedHeaders && ! fs.eof() && ! fs.fail()) { char line[1024]; fs.getline (line, sizeof(line)); int iSP = 0; while (line[iSP] == ' ') iSP++; if (line[iSP] == '\n' || ! line[iSP]) ; else if (line[iSP] == '#') { iSP++; while (line[iSP] == ' ') iSP++; if (line[iSP] == '\n' || ! line[iSP]) ; else addDescription (&line[iSP]); } else if (strstr (&line[iSP], "") != nullptr) { bFinishedHeaders = true; } else addEzsetCommand (&line[iSP]); } return ! fs.fail(); } bool PlotFile::headerWrite (std::iostream& fs) { if (! fs) { sys_error (ERR_WARNING, "Tried to write header with ! fs"); return false; } fs.seekp (0); // the file opens with the tag it has always closed with, so a reader // can identify it without relying on the extension. Files without the // opening tag (everything written before 7.0.0) are still read. fs << "\n"; fs << m_iNumColumns << " " << m_iNumRecords << "\n"; unsigned int i; for (i = 0; i < m_vecStrEzsetCommands.size(); i++) fs << m_vecStrEzsetCommands[i] << "\n"; for (i = 0; i < m_vecStrDescriptions.size(); i++) fs << "# " << m_vecStrDescriptions[i] << "\n"; if (! m_strDate.empty()) fs << "# Date: " << m_strDate << "\n"; return ! fs.fail(); } bool PlotFile::columnsWrite (std::iostream& fs) { if (! fs) { sys_error (ERR_WARNING, "Tried to columnWrite with !fs"); return false; } fs << "\n"; int iStride = m_iNumRecords; for (int iRec = 0; iRec < m_iNumRecords; iRec++) { for (int iCol = 0; iCol < m_iNumColumns; iCol++) fs << m_vecCurves [iRec + (iCol * iStride)] << " "; fs << "\n"; } fs << "\n"; fs << "\n"; return ! fs.fail(); } bool PlotFile::columnsRead (std::iostream& fs) { if (! fs) { sys_error (ERR_WARNING, "Tried to arrayDataRead with ! fs"); return false; } if (m_iNumColumns == 0 || m_iNumRecords == 0) { sys_error (ERR_WARNING, "Called PlotFile::columnsRead with 0 columns or records"); return false; } bool bTerminateEarly = false; for (int iRec = 0; iRec < m_iNumRecords; iRec++) { for (int iCol = 0; iCol < m_iNumColumns; iCol++) { if (fs.eof()) { bTerminateEarly = true; break; } if (fs.fail()) break; double d; fs >> d; m_vecCurves[ iRec + (iCol * m_iNumRecords) ] = d; } } return ! (bTerminateEarly || fs.fail()); } void PlotFile::printHeaders (std::ostream& os) const { os << "EZSet Commands\n"; for (unsigned int iEZ = 0; iEZ < m_vecStrEzsetCommands.size(); iEZ++) os << m_vecStrEzsetCommands[iEZ] << "\n"; os << "Descriptions\n"; for (unsigned int iDesc = 0; iDesc < m_vecStrDescriptions.size(); iDesc++) os << m_vecStrDescriptions[iDesc] << "\n"; } void PlotFile::printHeaders (std::ostringstream& os) const { os << "EZSet Commands\n"; for (unsigned int iEZ = 0; iEZ < m_vecStrEzsetCommands.size(); iEZ++) os << m_vecStrEzsetCommands[iEZ] << "\n"; os << "Descriptions\n"; for (unsigned int iDesc = 0; iDesc < m_vecStrDescriptions.size(); iDesc++) os << m_vecStrDescriptions[iDesc] << "\n"; } void PlotFile::printHeadersBrief (std::ostream& os) const { os << "EZSet Commands\n"; for (unsigned int iEZ = 0; iEZ < m_vecStrEzsetCommands.size(); iEZ++) os << m_vecStrEzsetCommands[iEZ] << "; "; if (m_vecStrEzsetCommands.size() > 0) os << "\n"; os << "Descriptions\n"; for (unsigned int iDesc = 0; iDesc < m_vecStrDescriptions.size(); iDesc++) os << m_vecStrDescriptions[iDesc] << "\n"; } void PlotFile::printHeadersBrief (std::ostringstream& os) const { os << "EZSet Commands\n"; for (unsigned int iEZ = 0; iEZ < m_vecStrEzsetCommands.size(); iEZ++) os << m_vecStrEzsetCommands[iEZ] << "; "; if (m_vecStrEzsetCommands.size() > 0) os << "\n"; os << "Descriptions\n"; for (unsigned int iDesc = 0; iDesc < m_vecStrDescriptions.size(); iDesc++) os << m_vecStrDescriptions[iDesc] << "\n"; } ctsim-7.0.0/libctsim/phantom.cpp0000644000175000017500000011657215253666241015655 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: phm.cpp ** Purpose: Routines for phantom objects ** Progammer: Kevin Rosenberg ** Date Started: Aug 1984 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #include "ct.h" #include #include #include #include #include const int PhantomElement::POINTS_PER_CIRCLE = 360; const double PhantomElement::SCALE_PELEM_EXTENT=0.000; // increase pelem limits by 0.5% //const double PhantomElement::SCALE_PELEM_EXTENT=0.005; // increase pelem limits by 0.5% const int Phantom::PHM_INVALID = -1; const int Phantom::PHM_HERMAN = 0; const int Phantom::PHM_SHEPP_LOGAN = 1; const int Phantom::PHM_UNITPULSE = 2; const char* Phantom::s_aszPhantomName[] = { "herman", "shepp-logan", "unit-pulse", }; const char* Phantom::s_aszPhantomTitle[] = { "Herman Head", "Shepp-Logan", "Unit Pulse", }; const int Phantom::s_iPhantomCount = sizeof(s_aszPhantomName) / sizeof(const char*); // CLASS IDENTIFICATION // Phantom // Phantom::Phantom () { init (); } Phantom::Phantom (std::string_view phmName) { init (); createFromPhantom (phmName); } void Phantom::init () { m_nPElem = 0; // A running minimum starts above every value it will meet and a running // maximum below: the largest and smallest a double holds say that, where // 1E30 was a guess that happened to be large enough. m_xmin = std::numeric_limits::max(); m_xmax = std::numeric_limits::lowest(); m_ymin = std::numeric_limits::max(); m_ymax = std::numeric_limits::lowest(); m_composition = P_PELEMS; m_fail = false; m_id = PHM_INVALID; } Phantom::~Phantom () { for (PElemIterator i = m_listPElem.begin(); i != m_listPElem.end(); i++) { delete *i; } } const char* Phantom::convertPhantomIDToName (int phmID) { static const char *name = ""; if (phmID >= 0 && phmID < s_iPhantomCount) return (s_aszPhantomName[phmID]); return (name); } const char* Phantom::convertPhantomIDToTitle (int phmID) { static const char *title = ""; // The title, not the name: this returned s_aszPhantomName, so everything // asking for a title to show a person got "herman" rather than "Herman // Head". The titles have been here all along and were never used. if (phmID >= 0 && phmID < s_iPhantomCount) return (s_aszPhantomTitle[phmID]); return (title); } int Phantom::convertNameToPhantomID (std::string_view phmName) { int id = PHM_INVALID; for (int i = 0; i < s_iPhantomCount; i++) { if (equalsIgnoringCase (phmName, s_aszPhantomName[i])) { id = i; break; } } return (id); } bool Phantom::createFromPhantom (std::string_view phmName) { int phmid = convertNameToPhantomID (phmName); if (phmid == PHM_INVALID) { m_fail = true; m_failMessage = "Invalid phantom name "; m_failMessage += phmName; return false; } m_name = phmName; createFromPhantom (phmid); return true; } bool Phantom::createFromPhantom (const int phmid) { switch (phmid) { case PHM_HERMAN: addStdHerman(); break; case PHM_SHEPP_LOGAN: addStdSheppLogan(); break; case PHM_UNITPULSE: m_composition = P_UNIT_PULSE; addPElem ("rectangle", 0., 0., 100., 100., 0., 0.); // outline addPElem ("ellipse", 0., 0., 1., 1., 0., 1.); // pulse break; default: m_fail = true; m_failMessage = "Illegal phantom id "; m_failMessage += phmid; return false; } m_id = phmid; return true; } /* METHOD IDENTIFICATION * createFromFile Add PhantomElements from file * * SYNOPSIS * createFromFile (filename) * * RETURNS * true if pelem were added * false if an pelem not added */ bool Phantom::addRasterPElem (std::string_view pngFilename, const double cx, const double cy, const double u, const double v, const double rot, const double attenMin, const double attenMax) { std::unique_ptr pElem (new PhantomElement (pngFilename, cx, cy, u, v, rot, attenMin, attenMax)); if (pElem->fail()) { sys_error (ERR_SEVERE, "{} [Phantom::addRasterPElem]", pElem->failMessage()); return false; } PhantomElement* pRaw = pElem.release(); // Phantom owns the element from here m_listPElem.push_back (pRaw); if (m_xmin > pRaw->xmin()) m_xmin = pRaw->xmin(); if (m_xmax < pRaw->xmax()) m_xmax = pRaw->xmax(); if (m_ymin > pRaw->ymin()) m_ymin = pRaw->ymin(); if (m_ymax < pRaw->ymax()) m_ymax = pRaw->ymax(); m_nPElem++; return true; } bool Phantom::createFromFile (std::string_view fname) { const std::string strFile (fname); std::ifstream is (strFile); if (! is) return false; m_name = fname; // Directory of the phantom file: relative raster filenames resolve // against it. std::filesystem knows what separates a path and what makes // one absolute on the platform it is running on, which a hand-written // test for a leading '/' does not. const std::string strBaseDir = fileDirname (fname); bool bGoodFile = true; std::string strLine; int iLineNum = 0; while (std::getline (is, strLine)) { iLineNum++; std::string::size_type iComment = strLine.find ('#'); // comments if (iComment != std::string::npos) strLine.erase (iComment); std::istringstream isLine (strLine); std::string strType; if (! (isLine >> strType)) continue; // blank line if (equalsIgnoringCase (strType, "raster")) { std::string strFilename; double cx, cy, u, v, rot, attenMin, attenMax; if (! (isLine >> strFilename >> cx >> cy >> u >> v >> rot >> attenMin >> attenMax)) { sys_error (ERR_WARNING, "Insufficient fields in raster record, line {} of phantom file {} [Phantom::createFromFile]", iLineNum, fname); bGoodFile = false; continue; } if (strFilename.length() > 1 && strFilename[0] == '"' && strFilename[strFilename.length()-1] == '"') strFilename = strFilename.substr (1, strFilename.length() - 2); const std::string strPath = filePathRelativeTo (strBaseDir, strFilename); if (! addRasterPElem (strPath, cx, cy, u, v, rot, attenMin, attenMax)) bGoodFile = false; continue; } double cx, cy, u, v, rot, dens; if (! (isLine >> cx >> cy >> u >> v >> rot >> dens)) { sys_error (ERR_WARNING, "Insufficient fields, line {} of phantom file {} [Phantom::createFromFile]", iLineNum, fname); bGoodFile = false; continue; } if (PhantomElement::convertNameToType (strType) == PELEM_INVALID) { sys_error (ERR_WARNING, "Unknown phantom element type \"{}\", line {} of phantom file {} [Phantom::createFromFile]", strType, iLineNum, fname); bGoodFile = false; continue; // skip instead of crashing } addPElem (strType, cx, cy, u, v, rot, dens); } return bGoodFile; } bool Phantom::fileWrite (std::string_view fname) { const std::string strFile (fname); fstream file (strFile, std::ios::out); if (! file.fail()) printDefinitions (file); return ! file.fail(); } /* NAME * addPElem Add pelem * * SYNOPSIS * addPElem (type, cx, cy, u, v, rot, atten) * char *type type of pelem (box, ellipse, etc) * double cx, cy pelem center * double u,v pelem size * double rot rotation angle of pelem (in degrees) * double atten x-ray attenuation cooefficient */ void Phantom::addPElem (std::string_view type, const double cx, const double cy, const double u, const double v, const double rot, const double atten) { PhmElemType pe_type = PhantomElement::convertNameToType (type); if (pe_type == PELEM_INVALID) { sys_error (ERR_WARNING, "Unknown PhantomElement type {} [PhantomElement::PhantomElement]", type); return; } PhantomElement *pelem = new PhantomElement (type, cx, cy, u, v, rot, atten); m_listPElem.push_back (pelem); // was push_front, which reversed // element order on every file round trip and // disagreed with addRasterPElem // update phantom limits if (m_xmin > pelem->xmin()) m_xmin = pelem->xmin(); if (m_xmax < pelem->xmax()) m_xmax = pelem->xmax(); if (m_ymin > pelem->ymin()) m_ymin = pelem->ymin(); if (m_ymax < pelem->ymax()) m_ymax = pelem->ymax(); m_nPElem++; } /*----------------------------------------------------------------------*/ /* Input-Output Routines */ /*----------------------------------------------------------------------*/ /* NAME * print Print vertices of Phantom pelems * * SYNOPSIS * print (phm) */ void Phantom::print (std::ostream& os) const { os << "Number of PElements: " << m_nPElem << "\n"; os << "Limits: xmin=" << m_xmin << ", ymin=" << m_ymin << ", xmax=" << m_xmax << ", ymax=" << m_ymax << "\n"; for (PElemConstIterator i = m_listPElem.begin(); i != m_listPElem.end(); i++) { const PhantomElement& rPE = **i; os << "PhantomElement: nPoints=" << rPE.nOutlinePoints(); os << ", atten=" << rPE.atten() << " rot=" << convertRadiansToDegrees (rPE.rot()) << "\n"; os << "xmin=" << rPE.xmin() << ", ymin=" << rPE.ymin() << ", xmax=" << rPE.xmax() << ", ymax=" << rPE.ymax() << "\n"; if (false) for (int i = 0; i < rPE.nOutlinePoints(); i++) os << rPE.xOutline()[i] << "," << rPE.yOutline()[i] << "\n"; } } void Phantom::print (std::ostringstream& os) const { os << "Number of PElements: " << m_nPElem << "\n"; os << "Limits: xmin=" << m_xmin << ", ymin=" << m_ymin << ", xmax=" << m_xmax << ", ymax=" << m_ymax << "\n"; for (PElemConstIterator i = m_listPElem.begin(); i != m_listPElem.end(); i++) { const PhantomElement& rPE = **i; os << "PhantomElement: nPoints=" << rPE.nOutlinePoints(); os << ", atten=" << rPE.atten() << " rot=" << convertRadiansToDegrees (rPE.rot()) << "\n"; os << "xmin=" << rPE.xmin() << ", ymin=" << rPE.ymin() << ", xmax=" << rPE.xmax() << ", ymax=" << rPE.ymax() << "\n"; if (false) for (int i = 0; i < rPE.nOutlinePoints(); i++) os << rPE.xOutline()[i] << "," << rPE.yOutline()[i] << "\n"; } } void Phantom::printDefinitions (std::ostream& os) const { for (PElemConstIterator i = m_listPElem.begin(); i != m_listPElem.end(); i++) { const PhantomElement& rPE = **i; rPE.printDefinition (os); } } void Phantom::printDefinitions (std::ostringstream& os) const { for (PElemConstIterator i = m_listPElem.begin(); i != m_listPElem.end(); i++) { const PhantomElement& rPE = **i; rPE.printDefinition (os); } } /* NAME * show Show vector outline of Phantom to user * * SYNOPSIS * show (pic) */ #ifdef HAVE_SGP void Phantom::show () const { SGPDriver driverSGP ("Phantom Show"); SGP sgp (driverSGP); show (sgp); std::cout << "Press return to continue"; cio_kb_getc(); } void Phantom::show (SGP& sgp) const { double wsize = m_xmax - m_xmin; if ((m_ymax - m_ymin) > wsize) wsize = m_ymax - m_ymin; wsize *= 1.01; double halfWindow = wsize / 2; double xcent = m_xmin + (m_xmax - m_xmin) / 2; double ycent = m_ymin + (m_ymax - m_ymin) / 2; sgp.setWindow (xcent - halfWindow, ycent - halfWindow, xcent + halfWindow, ycent + halfWindow); draw (sgp); } #endif /* NAME * draw Draw vector outline of Phantom * * SYNOPSIS * draw () */ #ifdef HAVE_SGP void Phantom::draw (SGP& sgp) const { for (PElemIterator i = m_listPElem.begin(); i != m_listPElem.end(); i++) sgp.polylineAbs (const_cast((*i)->xOutline()), const_cast((*i)->yOutline()), (*i)->nOutlinePoints()); } #endif /* NAME * addStdSheppLogan Make head phantom of Shepp-Logan * * REFERENCES * S. W. Rowland, "Computer Implementation of Image Reconstruction * Formulas", in "Image Reconstruction from Projections: Implementation * and Applications", edited by G. T. Herman, 1978. */ void Phantom::addStdSheppLogan () { addPElem ("ellipse", 0.0000, 0.0000, 0.6900, 0.9200, 0.0, 1.00); addPElem ("ellipse", 0.0000, -0.0184, 0.6624, 0.8740, 0.0, -0.98); addPElem ("ellipse", 0.2200, 0.0000, 0.1100, 0.3100, -18.0, -0.02); addPElem ("ellipse", -0.2200, 0.0000, 0.1600, 0.4100, 18.0, -0.02); addPElem ("ellipse", 0.0000, 0.3500, 0.2100, 0.2500, 0.0, 0.01); addPElem ("ellipse", 0.0000, 0.1000, 0.0460, 0.0460, 0.0, 0.01); addPElem ("ellipse", 0.0000, -0.1000, 0.0460, 0.0460, 0.0, 0.01); addPElem ("ellipse", -0.0800, -0.6050, 0.0460, 0.0230, 0.0, 0.01); addPElem ("ellipse", 0.0000, -0.6050, 0.0230, 0.0230, 0.0, 0.01); addPElem ("ellipse", 0.0600, -0.6050, 0.0230, 0.0230, 0.0, 0.01); addPElem ("ellipse", 0.5538, -0.3858, 0.0330, 0.2060, -18.0, 0.03); } /* NAME * addStdHerman Standard head phantom of G. T. Herman * * REFERENCES * G. T. Herman, "Image Reconstructions from Projections: The Fundementals * of Computed Tomography", 1979. */ void Phantom::addStdHerman () { addPElem ("ellipse", 0.000, 1.50, 0.375, 0.3000, 90.00, -0.003); addPElem ("ellipse", 0.675, -0.75, 0.225, 0.1500, 140.00, 0.010); addPElem ("ellipse", 0.750, 1.50, 0.375, 0.2250, 50.00, 0.003); addPElem ("segment", 1.375, -7.50, 1.100, 0.6250, 19.20, -0.204); addPElem ("segment", 1.375, -7.50, 1.100, 4.3200, 19.21, 0.204); addPElem ("segment", 0.000, -2.25, 1.125, 0.3750, 0.00, -0.003); addPElem ("segment", 0.000, -2.25, 1.125, 3.0000, 0.00, 0.003); addPElem ("segment", -1.000, 3.75, 1.000, 0.5000, 135.00, -0.003); addPElem ("segment", -1.000, 3.75, 1.000, 3.0000, 135.00, 0.003); addPElem ("segment", 1.000, 3.75, 1.000, 0.5000, 225.00, -0.003); addPElem ("segment", 1.000, 3.75, 1.000, 3.0000, 225.00, 0.003); addPElem ("triangle", 5.025, 3.75, 1.125, 0.5000, 110.75, 0.206); addPElem ("triangle",-5.025, 3.75, 1.125, 0.9000,-110.75, 0.206); addPElem ("ellipse", 0.000, 0.00, 8.625, 6.4687, 90.00, 0.416); addPElem ("ellipse", 0.000, 0.00, 7.875, 5.7187, 90.00, -0.206); } /* NAME * convertToImagefile Make image array from Phantom * * SYNOPSIS * pic_to_imagefile (pic, im, nsample) * Phantom& pic Phantom definitions * ImageFile *im Computed pixel array * int nsample Number of samples along each axis for each pixel * (total samples per pixel = nsample * nsample) */ void Phantom::convertToImagefile (ImageFile& im, double dViewRatio, const int in_nsample, const int trace) const { convertToImagefile (im, dViewRatio, in_nsample, trace, 0, im.nx(), true); } void Phantom::convertToImagefile (ImageFile& im, const double dViewRatio, const int in_nsample, const int trace, const int colStart, const int colCount, bool bStoreAtColumnPos) const { int iStorageOffset = (bStoreAtColumnPos ? colStart : 0); convertToImagefile (im, im.nx(), dViewRatio, in_nsample, trace, colStart, colCount, iStorageOffset); } void Phantom::convertToImagefile (ImageFile& im, const int iTotalRasterCols, const double dViewRatio, const int in_nsample, const int trace, const int colStart, const int colCount, int iStorageOffset) const { const int nx = im.nx(); const int ny = im.ny(); if (nx < 2 || ny < 2) return; int nsample = in_nsample; if (nsample < 1) nsample = 1; double dx = m_xmax - m_xmin; double dy = m_ymax - m_ymin; double xcent = m_xmin + dx / 2; double ycent = m_ymin + dy / 2; double dHalflen = dViewRatio * (getDiameterBoundaryCircle() / SQRT2 / 2); double xmin = xcent - dHalflen; double xmax = xcent + dHalflen; double ymin = ycent - dHalflen; double ymax = ycent + dHalflen; // Each pixel holds the average of the intensity of the cell with (ix,iy) at the center of the pixel // Set major increments so that the last cell v[nx-1][ny-1] will start at xmax - xinc, ymax - yinc). // Set minor increments so that sample points are centered in cell double xinc = (xmax - xmin) / (iTotalRasterCols); double yinc = (ymax - ymin) / ny; double kxinc = xinc / nsample; /* interval between samples */ double kyinc = yinc / nsample; double kxofs = kxinc / 2; /* offset of 1st point */ double kyofs = kyinc / 2; im.setAxisExtent (xmin, xmax, ymin, ymax); im.setAxisIncrement (xinc, yinc); ImageFileArray v = im.getArray(); for (int ix = 0; ix < colCount; ix++) { int iColStore = ix + iStorageOffset; ImageFileColumn vCol = v[iColStore]; for (int iy = 0; iy < ny; iy++) *vCol++ = 0; } #if HAVE_OPENMP double x_start = xmin + (colStart * xinc); for (PElemConstIterator pelem = m_listPElem.begin(); pelem != m_listPElem.end(); pelem++) { const PhantomElement& rPElem = **pelem; #pragma omp parallel for for (int ix = 0; ix < colCount; ix++) { // x must be computed from ix: this loop is the OpenMP parallel one, so a // running accumulator would be shared across threads. Everything inside // is thread-private and therefore increments instead of multiplying. double x = x_start + ix * xinc; int iColStore = ix + iStorageOffset; ImageFileColumn vCol = v[iColStore]; double y = ymin; for (int iy = 0; iy < ny; iy++, y += yinc) { double dAtten = 0; double xi = x + kxofs; for (int kx = 0; kx < nsample; kx++, xi += kxinc) { double yi = y + kyofs; for (int ky = 0; ky < nsample; ky++, yi += kyinc) { if (rPElem.isRaster()) dAtten += rPElem.attenuationAt (xi, yi); else if (rPElem.isPointInside (xi, yi, PHM_COORD)) dAtten += rPElem.atten(); } // ky } // kx *vCol++ += dAtten; } /* iy */ } /* ix */ } /* pelem */ #else double x_start = xmin + (colStart * xinc); for (PElemConstIterator pelem = m_listPElem.begin(); pelem != m_listPElem.end(); pelem++) { const PhantomElement& rPElem = **pelem; double x, y, xi, yi; int ix, iy, kx, ky; for (ix = 0, x = x_start; ix < colCount; ix++, x += xinc) { int iColStore = ix + iStorageOffset; ImageFileColumn vCol = v[iColStore]; for (iy = 0, y = ymin; iy < ny; iy++, y += yinc) { double dAtten = 0; for (kx = 0, xi = x + kxofs; kx < nsample; kx++, xi += kxinc) { for (ky = 0, yi = y + kyofs; ky < nsample; ky++, yi += kyinc) { if (rPElem.isRaster()) dAtten += rPElem.attenuationAt (xi, yi); else if (rPElem.isPointInside (xi, yi, PHM_COORD)) dAtten += rPElem.atten(); } } // for kx *vCol++ += dAtten; } /* for iy */ } /* for ix */ } /* for pelem */ #endif if (nsample > 1) { double factor = 1.0 / static_cast(nsample * nsample); #if HAVE_OPENMP #pragma omp parallel for #endif for (int ix = 0; ix < colCount; ix++) { int iColStore = ix + iStorageOffset; ImageFileColumn vCol = v[iColStore]; for (int iy = 0; iy < ny; iy++) *vCol++ *= factor; } } } //////////////////////////////////////////////////////////////////////////////////////////////////////// // CLASS IDENTIFICATION // // PhantomElement // // PURPOSE // //////////////////////////////////////////////////////////////////////////////////////////////////////// PhantomElement::PhantomElement (std::string_view type, const double cx, const double cy, const double u, const double v, const double rot, const double atten) : m_cx(cx), m_cy(cy), m_u(u), m_v(v), m_atten(atten), m_nPoints(0) { m_rot = convertDegreesToRadians (rot); // convert angle to radians m_type = convertNameToType (type); makeTransformMatrices (); // calc transform matrices between phantom and normalized phantomelement makeVectorOutline (); // calculate vector outline of pelem m_rectLimits[0] = m_xmin; m_rectLimits[1] = m_ymin; m_rectLimits[2] = m_xmax; m_rectLimits[3] = m_ymax; } PhantomElement::~PhantomElement () { } void PhantomElement::printDefinition (std::ostream& os) const { if (m_type == PELEM_RASTER) { os << "raster " << m_strRasterFile << " " << m_cx << " " << m_cy << " " << m_u << " " << m_v << " " << convertRadiansToDegrees (m_rot) << " " << m_dRasterAttenMin << " " << m_dRasterAttenMax << "\n"; return; } os << convertTypeToName (m_type) << " " << m_cx << " " << m_cy << " " << m_u << " " << m_v << " " << convertRadiansToDegrees (m_rot) << " " << m_atten << "\n"; } void PhantomElement::printDefinition (std::ostringstream& os) const { if (m_type == PELEM_RASTER) { os << "raster " << m_strRasterFile << " " << m_cx << " " << m_cy << " " << m_u << " " << m_v << " " << convertRadiansToDegrees (m_rot) << " " << m_dRasterAttenMin << " " << m_dRasterAttenMax << "\n"; return; } os << convertTypeToName (m_type) << " " << m_cx << " " << m_cy << " " << m_u << " " << m_v << " " << convertRadiansToDegrees (m_rot) << " " << m_atten << "\n"; } PhmElemType PhantomElement::convertNameToType (std::string_view name) { PhmElemType type = PELEM_INVALID; if (equalsIgnoringCase (name, "rectangle")) type = PELEM_RECTANGLE; else if (equalsIgnoringCase (name, "triangle")) type = PELEM_TRIANGLE; else if (equalsIgnoringCase (name, "ellipse")) type = PELEM_ELLIPSE; else if (equalsIgnoringCase (name, "sector")) type = PELEM_SECTOR; else if (equalsIgnoringCase (name, "segment")) type = PELEM_SEGMENT; else if (equalsIgnoringCase (name, "raster")) type = PELEM_RASTER; return (type); } const char* PhantomElement::convertTypeToName (PhmElemType iType) { static const char* pszType = "Unknown"; if (iType == PELEM_RECTANGLE) pszType = "rectangle"; else if (iType == PELEM_TRIANGLE) pszType = "triangle"; else if (iType == PELEM_ELLIPSE) pszType = "ellipse"; else if (iType == PELEM_SECTOR) pszType = "sector"; else if (iType == PELEM_SEGMENT) pszType = "segment"; else if (iType == PELEM_RASTER) pszType = "raster"; return pszType; } // PhantomElement raster constructor (6.1.0). Loads a PNG image and maps its // grayscale range [0,1] onto [attenMin, attenMax]. PhantomElement::PhantomElement (std::string_view pngFilename, const double cx, const double cy, const double u, const double v, const double rot, const double attenMin, const double attenMax) : m_cx(cx), m_cy(cy), m_u(u), m_v(v), m_atten(attenMax), m_nPoints(0) { m_rot = convertDegreesToRadians (rot); m_type = PELEM_RASTER; m_dRasterAttenMin = attenMin; m_dRasterAttenMax = attenMax; m_strRasterFile = pngFilename; if (! loadRasterPNG (pngFilename)) { m_bFail = true; if (m_strFailMessage.length() == 0) m_strFailMessage = std::string("Unable to read PNG raster phantom file ") + std::string (pngFilename); } makeTransformMatrices (); makeVectorOutline (); // rectangular outline, as for PELEM_RECTANGLE m_rectLimits[0] = m_xmin; m_rectLimits[1] = m_ymin; m_rectLimits[2] = m_xmax; m_rectLimits[3] = m_ymax; } bool Phantom::createFromPNG (std::string_view pngFilename, std::string_view svReferenceName) { init(); ImageFile imPNG; if (! imPNG.readImagePNG (pngFilename)) { sys_error (ERR_WARNING, "Unable to read PNG file {} [Phantom::createFromPNG]", pngFilename); return false; } const int nx = static_cast (imPNG.nx()); const int ny = static_cast (imPNG.ny()); if (nx <= 0 || ny <= 0) { sys_error (ERR_WARNING, "Empty PNG file {} [Phantom::createFromPNG]", pngFilename); return false; } // Half-extents of nx/2 and ny/2 make the image nx by ny world units across, // centred on the origin; 0 and 1 map the PNG's full range to attenuation. if (! addRasterPElem (pngFilename, 0., 0., nx / 2., ny / 2., 0., 0., 1.)) return false; // A view is never null; empty is how "not given" is said now. if (! svReferenceName.empty()) m_listPElem.back()->setRasterFilename (svReferenceName); m_name = std::string (svReferenceName.empty() ? pngFilename : svReferenceName); return true; } bool PhantomElement::loadRasterPNG (std::string_view filename) { #ifdef HAVE_PNG ImageFile imPNG; if (! imPNG.readImagePNG (filename)) return false; m_nRasterX = imPNG.nx(); m_nRasterY = imPNG.ny(); if (m_nRasterX <= 0 || m_nRasterY <= 0) { m_strFailMessage = std::string("Empty PNG raster phantom file ") + std::string (filename); return false; } const double dRange = m_dRasterAttenMax - m_dRasterAttenMin; ImageFileArrayConst vPNG = imPNG.getArray(); m_vRasterAtten.resize (static_cast(m_nRasterX) * m_nRasterY); for (int ix = 0; ix < m_nRasterX; ix++) for (int iy = 0; iy < m_nRasterY; iy++) m_vRasterAtten[static_cast(ix) * m_nRasterY + iy] = m_dRasterAttenMin + vPNG[ix][iy] * dRange; return true; #else m_strFailMessage = "Raster phantom elements require a libpng-enabled build"; return false; #endif } // Attenuation at world point (x,y); 0 outside the element. double PhantomElement::attenuationAt (double x, double y) const { if (m_type != PELEM_RASTER) return isPointInside (x, y, PHM_COORD) ? m_atten : 0.; if (m_vRasterAtten.empty()) return 0.; xform_mtx2 (m_xformPhmToObj, x, y); // normalized element coords, [-1,1] if (x < -1. || x > 1. || y < -1. || y > 1.) return 0.; int ix = static_cast ((x + 1.) * 0.5 * m_nRasterX); int iy = static_cast ((y + 1.) * 0.5 * m_nRasterY); if (ix < 0) ix = 0; else if (ix >= m_nRasterX) ix = m_nRasterX - 1; if (iy < 0) iy = 0; else if (iy >= m_nRasterY) iy = m_nRasterY - 1; return m_vRasterAtten[static_cast(ix) * m_nRasterY + iy]; } // Exact line integral of attenuation along the world segment (x1,y1)-(x2,y2). // For a raster this is Siddon's algorithm run in normalized element coordinates // (where the pixel grid is axis-aligned) with the parameter t carried back to // world length, so rotation and non-square pixels are handled exactly. double PhantomElement::lineIntegral (double x1, double y1, double x2, double y2) const { const double dWorldLength = ::sqrt ((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1)); if (dWorldLength <= 0.) return 0.; if (m_type != PELEM_RASTER) { double cx1 = x1, cy1 = y1, cx2 = x2, cy2 = y2; if (! clipLineWorldCoords (cx1, cy1, cx2, cy2)) return 0.; return lineLength (cx1, cy1, cx2, cy2) * m_atten; } if (m_vRasterAtten.empty()) return 0.; double n1x = x1, n1y = y1, n2x = x2, n2y = y2; xform_mtx2 (m_xformPhmToObj, n1x, n1y); xform_mtx2 (m_xformPhmToObj, n2x, n2y); const double dx = n2x - n1x, dy = n2y - n1y; double tmin = 0., tmax = 1.; const double dTiny = 1E-15; if (fabs(dx) < dTiny) { if (n1x < -1. || n1x > 1.) return 0.; } else { double ta = (-1. - n1x) / dx, tb = (1. - n1x) / dx; if (ta > tb) std::swap (ta, tb); if (ta > tmin) tmin = ta; if (tb < tmax) tmax = tb; } if (fabs(dy) < dTiny) { if (n1y < -1. || n1y > 1.) return 0.; } else { double ta = (-1. - n1y) / dy, tb = (1. - n1y) / dy; if (ta > tb) std::swap (ta, tb); if (ta > tmin) tmin = ta; if (tb < tmax) tmax = tb; } if (tmin >= tmax) return 0.; const double dCellX = 2. / m_nRasterX; const double dCellY = 2. / m_nRasterY; double dSum = 0.; double t = tmin; const double dEps = 1E-12; while (t < tmax - dEps) { const double tMid = t + std::min (dEps, (tmax - t) * 0.5); const double px = n1x + tMid * dx; const double py = n1y + tMid * dy; int ix = static_cast ((px + 1.) / dCellX); int iy = static_cast ((py + 1.) / dCellY); if (ix < 0) ix = 0; else if (ix >= m_nRasterX) ix = m_nRasterX - 1; if (iy < 0) iy = 0; else if (iy >= m_nRasterY) iy = m_nRasterY - 1; double tExit = tmax; if (fabs(dx) >= dTiny) { const double dPlaneX = (dx > 0) ? (-1. + (ix + 1) * dCellX) : (-1. + ix * dCellX); const double te = (dPlaneX - n1x) / dx; if (te > t + dEps && te < tExit) tExit = te; } if (fabs(dy) >= dTiny) { const double dPlaneY = (dy > 0) ? (-1. + (iy + 1) * dCellY) : (-1. + iy * dCellY); const double te = (dPlaneY - n1y) / dy; if (te > t + dEps && te < tExit) tExit = te; } if (tExit > tmax) tExit = tmax; if (tExit <= t) break; dSum += m_vRasterAtten[static_cast(ix) * m_nRasterY + iy] * (tExit - t) * dWorldLength; t = tExit; } return dSum; } void PhantomElement::makeTransformMatrices () { GRFMTX_2D temp; // To map normalized Pelem coords to world Phantom // scale by (u, v) // rotate by rot // translate by (cx, cy) scale_mtx2 (m_xformObjToPhm, m_u, m_v); rot_mtx2 (temp, m_rot); mult_mtx2 (m_xformObjToPhm, temp, m_xformObjToPhm); xlat_mtx2 (temp, m_cx, m_cy); mult_mtx2 (m_xformObjToPhm, temp, m_xformObjToPhm); // to map world Phantom coodinates to normalized PElem coords // translate by (-cx, -cy) // rotate by -rot // scale by (1/u, 1/v) xlat_mtx2 (m_xformPhmToObj, -m_cx, -m_cy); rot_mtx2 (temp, -m_rot); mult_mtx2 (m_xformPhmToObj, temp, m_xformPhmToObj); scale_mtx2 (temp, 1 / m_u, 1 / m_v); mult_mtx2 (m_xformPhmToObj, temp, m_xformPhmToObj); } /* NAME * pelem_make_points INTERNAL routine to calculate point array for an pelem * * SYNOPSIS * makepelempts (pelem) * PELEM *pelem pelem whose points we are calculating * * NOTES * Called by phm_add_pelem() */ void PhantomElement::makeVectorOutline () { double radius, theta, start, stop; double xfact, yfact; int cpts; m_nPoints = 0; switch (m_type) { case PELEM_RASTER: // raster occupies a rectangle case PELEM_RECTANGLE: m_nPoints = 5; m_vXOutline.resize (m_nPoints); m_vYOutline.resize (m_nPoints); m_vXOutline[0] =-m_u; m_vYOutline[0] =-m_v; m_vXOutline[1] = m_u; m_vYOutline[1] =-m_v; m_vXOutline[2] = m_u; m_vYOutline[2] = m_v; m_vXOutline[3] =-m_u; m_vYOutline[3] = m_v; m_vXOutline[4] =-m_u; m_vYOutline[4] =-m_v; break; case PELEM_TRIANGLE: m_nPoints = 4; m_vXOutline.resize (m_nPoints); m_vYOutline.resize (m_nPoints); m_vXOutline[0] =-m_u; m_vYOutline[0] = 0.0; m_vXOutline[1] = m_u; m_vYOutline[1] = 0.0; m_vXOutline[2] = 0.0; m_vYOutline[2] = m_v; m_vXOutline[3] =-m_u; m_vYOutline[3] = 0.0; break; case PELEM_ELLIPSE: cpts = numCirclePoints (TWOPI); m_nPoints = cpts; m_vXOutline.resize (m_nPoints); m_vYOutline.resize (m_nPoints); calcEllipsePoints (m_vXOutline.data(), m_vYOutline.data(), cpts, m_u, m_v); break; case PELEM_SECTOR: radius = sqrt(m_u * m_u + m_v * m_v); theta = atan(m_u / m_v); // angle with y-axis start = 3.0 * HALFPI - theta; stop = 3.0 * HALFPI + theta; cpts = numCirclePoints (stop - start); m_nPoints = 3 + cpts; m_vXOutline.resize (m_nPoints); m_vYOutline.resize (m_nPoints); m_vXOutline[0] = 0.0; m_vYOutline[0] = m_v; m_vXOutline[1] =-m_u; m_vYOutline[1] = 0.0; calcArcPoints (&m_vXOutline[2], &m_vYOutline[2], cpts, 0.0, m_v, radius, start, stop); m_vXOutline[cpts + 2] = 0.0; m_vYOutline[cpts + 2] = m_v; break; case PELEM_SEGMENT: radius = sqrt(m_u * m_u + m_v * m_v); theta = atan (m_u / m_v); // angle with y-axis start = 3.0 * HALFPI - theta; stop = 3.0 * HALFPI + theta; cpts = numCirclePoints (stop - start); m_nPoints = cpts + 1; m_vXOutline.resize (m_nPoints); m_vYOutline.resize (m_nPoints); calcArcPoints (m_vXOutline.data(), m_vYOutline.data(), cpts, 0.0, m_v, radius, start, stop); m_vXOutline[cpts] = -m_u; m_vYOutline[cpts] = 0.0; break; default: sys_error (ERR_WARNING, "Illegal phantom element type {} [makeVectorOutline]", static_cast (m_type)); return; } rotate2d (m_vXOutline.data(), m_vYOutline.data(), m_nPoints, m_rot); xlat2d (m_vXOutline.data(), m_vYOutline.data(), m_nPoints, m_cx, m_cy); minmax_array (m_vXOutline.data(), m_nPoints, m_xmin, m_xmax); minmax_array (m_vYOutline.data(), m_nPoints, m_ymin, m_ymax); // increase pelem extent by SCALE_PELEM_EXTENT to eliminate chance of // missing actual pelem maximum due to polygonal sampling xfact = (m_xmax - m_xmin) * SCALE_PELEM_EXTENT; yfact = (m_ymax - m_ymin) * SCALE_PELEM_EXTENT; m_xmin -= xfact; m_ymin -= yfact; m_xmax += xfact; m_ymax += yfact; } /* NAME * calc_arc Calculate outline of a arc of a circle * * SYNOPSIS * calc_arc (x, y, xcent, ycent, pts, r, start, stop) * double x[], y[]; Array of points * int pts Number of points in array * double xcent, ycent Center of cirlce * double r Radius of circle * double start, stop Beginning & ending angles */ void PhantomElement::calcArcPoints (double x[], double y[], const int pts, const double xcent, const double ycent, const double r, const double start, const double stop) { if (r <= 0.0) sys_error (ERR_WARNING, "negative or zero radius in calc_arc()"); double theta = (stop - start) / (pts - 1); // angle incr. between points double c = cos(theta); double s = sin(theta); x[0] = r * cos (start) + xcent; y[0] = r * sin (start) + ycent; double xp = x[0] - xcent; double yp = y[0] - ycent; for (int i = 1; i < pts; i++) { double xc = c * xp - s * yp; double yc = s * xp + c * yp; x[i] = xc + xcent; y[i] = yc + ycent; xp = xc; yp = yc; } } // NAME // PhantomElement::calcEllipsePoints Calculate outline of a ellipse // // SYNOPSIS // calcEllipsePoints () // void PhantomElement::calcEllipsePoints (double x[], double y[], const int pts, const double u, const double v) { calcArcPoints (x, y, m_nPoints, 0.0, 0.0, 1.0, 0.0, TWOPI); // make a unit circle scale2d (x, y, m_nPoints, m_u, m_v); // scale to ellipse } /* NAME * circle_pts Calculate number of points to use for circle segment * * SYNOPSIS * n = circle_pts (theta) * int n Number of points to use for arc * double theta Length of arc in radians */ int PhantomElement::numCirclePoints (double theta) { theta = clamp (theta, 0., TWOPI); return static_cast (POINTS_PER_CIRCLE * theta / TWOPI + 1.5); } bool PhantomElement::clipLineWorldCoords (double& x1, double& y1, double& x2, double &y2) const { /* check if ray is outside of pelem extents */ double cx1 = x1, cy1 = y1, cx2 = x2, cy2 = y2; if (! clip_rect (cx1, cy1, cx2, cy2, m_rectLimits)) return false; // convert phantom coordinates to pelem coordinates xform_mtx2 (m_xformPhmToObj, x1, y1); xform_mtx2 (m_xformPhmToObj, x2, y2); if (! clipLineNormalizedCoords (x1, y1, x2, y2)) return false; // convert standard pelem coordinates back to phantom coordinates xform_mtx2 (m_xformObjToPhm, x1, y1); xform_mtx2 (m_xformObjToPhm, x2, y2); return true; } /* NAME * pelem_clip_line Clip pelem against an arbitrary line * * SYNOPSIS * pelem_clip_line (pelem, x1, y1, x2, y2) * PhantomElement& pelem; Pelem to be clipped * double *x1, *y1, *x2, *y2 Endpoints of line to be clipped * * RETURNS * true if line passes through pelem * (x1, y1, x2, y2 hold coordinates of new line) * false if line do not pass through pelem * (x1, y1, x2, y2 are undefined) */ bool PhantomElement::clipLineNormalizedCoords (double& x1, double& y1, double& x2, double& y2) const { bool accept = false; switch (m_type) { case PELEM_RASTER: // raster occupies a rectangle case PELEM_RECTANGLE: double rect[4]; rect[0] = -1.0; rect[1] = -1.0; rect[2] = 1.0; rect[3] = 1.0; accept = clip_rect (x1, y1, x2, y2, rect); break; case PELEM_ELLIPSE: accept = clip_circle (x1, y1, x2, y2, 0.0, 0.0, 1.0, 0.0, 0.0); break; case PELEM_TRIANGLE: accept = clip_triangle (x1, y1, x2, y2, 1.0, 1.0, true); break; case PELEM_SEGMENT: accept = clip_segment (x1, y1, x2, y2, m_u, m_v); break; case PELEM_SECTOR: accept = clip_sector (x1, y1, x2, y2, m_u, m_v); break; default: sys_error (ERR_WARNING, "Illegal pelem type {} [pelem_clip_line]", static_cast (m_type)); break; } return(accept); } // METHOD IDENTIFICATION // PhantomElement::isPointInside Check if point is inside pelem // // SYNOPSIS // is_point_inside (pelem, x, y, coord_type) // double x, y Point to see if lies in pelem // int coord_type Coordinate type (PELEM_COORD or PHM_COORD) // // RETURNS // true if point lies within pelem // false if point lies outside of pelem bool PhantomElement::isPointInside (double x, double y, const CoordType coord_type) const { if (coord_type == PHM_COORD) { xform_mtx2 (m_xformPhmToObj, x, y); } else if (coord_type != PELEM_COORD) { sys_error (ERR_WARNING, "Illegal coordinate type in pelem_is_point_inside"); return (false); } switch (m_type) { case PELEM_RASTER: // raster occupies a rectangle case PELEM_RECTANGLE: if (x > 1. || x < -1. || y > 1. || y < -1.) return (false); else return (true); break; case PELEM_TRIANGLE: if (y < 0. || y > 1. - x || y > 1. + x) return (false); else return (true); break; case PELEM_ELLIPSE: if (x > 1. || x < -1. || y > 1. || y < -1.) return (false); if (x * x + y * y > 1.) // check if inside unit circle return (false); else return (true); break; // for clipping segments & sectors, must NOT scale by (1/u, 1/v) // because this destroys information about size of arc component case PELEM_SEGMENT: if (x > 1. || x < -1. || y > 0.) return (false); // clip against y > 0 x *= m_u; // put back u & v scale y *= m_v; if (x * x + (y-m_v) * (y-m_v) > m_u * m_u + m_v * m_v) return (false); // clip against circle, r = sqrt(@) else return (true); break; case PELEM_SECTOR: if (x > 1. || x < -1. || y > 1.) // extent return (false); if (y > 1. - x || y > 1. + x) // triangle return (false); // clip against triangle x *= m_u; // circle: put back u & v scale y *= m_v; if (x * x + (y-m_v) * (y-m_v) > m_u * m_u + m_v * m_v) return (false); // clip against circle else return (true); break; default: sys_error (ERR_WARNING, "Illegal pelem type in pelem_is_point_inside()"); break; } return (false); } ctsim-7.0.0/libctsim/dicom.cpp0000644000175000017500000003176715253666241015304 0ustar kevinkevin/***************************************************************************** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ // DICOM import and export. // // DICOM import and export through DCMTK. Through 6.2.1 this file used the CTN // (Mallinckrodt Central Test Node) library, which is unmaintained and packaged // only for Debian. Behaviour is preserved, with two deliberate corrections // noted at their call sites: Rescale Slope/Intercept are now actually applied // on import, and the exported SOP Class UID is the real CT Image Storage UID. #ifdef HAVE_CONFIG_H #include "config.h" #endif #ifdef HAVE_DCMTK #include "dicom.h" #include "imagefile.h" #include "projections.h" #include #include // --------------------------------------------------------------------------- // DicomImporter // --------------------------------------------------------------------------- DicomImporter::DicomImporter (std::string_view svFile) : m_strFilename(svFile), m_bFail(false), m_iContents(DICOM_CONTENTS_INVALID), m_pImageFile(nullptr), m_pProjections(nullptr), m_pFile(nullptr) { m_pFile = new DcmFileFormat; OFCondition cond = m_pFile->loadFile (std::string (svFile).c_str()); if (cond.bad()) { m_bFail = true; m_strFailMessage = "Unable to read DICOM file "; m_strFailMessage += m_strFilename; m_strFailMessage += ": "; m_strFailMessage += cond.text(); return; } DcmDataset* pDataset = m_pFile->getDataset(); // An image carries all six pixel-description attributes; a Somatom raw scan // carries none of them, which is how the two contents are told apart. Uint16 iNRows = 0, iNCols = 0, iBitsAllocated = 0; Uint16 iBitsStored = 0, iHighBit = 0, iPixRep = 0; if (pDataset->findAndGetUint16 (DCM_Rows, iNRows).good() && pDataset->findAndGetUint16 (DCM_Columns, iNCols).good() && pDataset->findAndGetUint16 (DCM_BitsAllocated, iBitsAllocated).good() && pDataset->findAndGetUint16 (DCM_BitsStored, iBitsStored).good() && pDataset->findAndGetUint16 (DCM_HighBit, iHighBit).good() && pDataset->findAndGetUint16 (DCM_PixelRepresentation, iPixRep).good()) { loadImage (iNRows, iNCols, iBitsAllocated, iBitsStored, iHighBit, iPixRep); return; } DcmElement* pElement = nullptr; const DcmTagKey somatomTag (TAG_GROUP_SOMATOM, TAG_MEMBER_SOMATOM_DATA); if (pDataset->findAndGetElement (somatomTag, pElement).good() && pElement) loadProjections(); } DicomImporter::~DicomImporter () { delete m_pFile; } void DicomImporter::loadImage (unsigned short iNRows, unsigned short iNCols, unsigned short iBitsAllocated, unsigned short iBitsStored, unsigned short iHighBit, unsigned short iPixRep) { DcmDataset* pDataset = m_pFile->getDataset(); // SamplesPerPixel is optional in the files CTSim reads (the Siemens images // omit it). findAndGetUint16 zeroes its output when the tag is absent, so // the default has to be applied after the call, not before it. Uint16 iSamplesPerPixel = 1; if (pDataset->findAndGetUint16 (DCM_SamplesPerPixel, iSamplesPerPixel).bad() || iSamplesPerPixel == 0) iSamplesPerPixel = 1; if (iSamplesPerPixel != 1) { m_bFail = true; m_strFailMessage = "Can only read single-sample images"; return; } if (iBitsAllocated != 8 && iBitsAllocated != 16) { m_bFail = true; m_strFailMessage = "Can only read 8-bit and 16-bit images"; return; } // these are now actually applied. The older implementation read them into a // zero-length buffer, so slope/intercept silently stayed 1 and 0 and stored // values were returned instead of the real units. double dRescaleSlope = 1., dRescaleIntercept = 0.; pDataset->findAndGetFloat64 (DCM_RescaleSlope, dRescaleSlope); pDataset->findAndGetFloat64 (DCM_RescaleIntercept, dRescaleIntercept); const unsigned long lExpectedPixels = static_cast(iNRows) * iNCols * iSamplesPerPixel; // Mask off bits above BitsStored, as the older implementation did. unsigned int iMaskLength = iBitsStored; if (iMaskLength > 8) iMaskLength -= 8; const unsigned int iHighByteMask = (1u << iMaskLength) - 1; const unsigned int iValueMask = (iBitsAllocated == 16) ? ((iHighByteMask << 8) | 0xFFu) : iHighByteMask; m_pImageFile = new ImageFile (iNCols, iNRows); ImageFileArray v = m_pImageFile->getArray(); const double dScale = static_cast(1 << iBitsStored); if (iBitsAllocated == 16) { const Uint16* pPixels = nullptr; unsigned long lCount = 0; if (pDataset->findAndGetUint16Array (DCM_PixelData, pPixels, &lCount).bad() || ! pPixels) { m_bFail = true; m_strFailMessage = "Can't read pixel data"; return; } if (lCount < lExpectedPixels) { m_bFail = true; m_strFailMessage = "Too little pixel data supplied"; return; } for (int iy = 0; iy < iNRows; iy++) { for (int ix = 0; ix < iNCols; ix++) { const unsigned int iV = pPixels[iy * iNCols + ix] & iValueMask; v[ix][iNRows - 1 - iy] = iV * dRescaleSlope + dRescaleIntercept; } } } else { const Uint8* pPixels = nullptr; unsigned long lCount = 0; if (pDataset->findAndGetUint8Array (DCM_PixelData, pPixels, &lCount).bad() || ! pPixels) { m_bFail = true; m_strFailMessage = "Can't read pixel data"; return; } if (lCount < lExpectedPixels) { m_bFail = true; m_strFailMessage = "Too little pixel data supplied"; return; } for (int iy = 0; iy < iNRows; iy++) { for (int ix = 0; ix < iNCols; ix++) { const unsigned int cV = pPixels[iy * iNCols + ix] & iValueMask; v[ix][iy] = cV / dScale; } } } m_iContents = DICOM_CONTENTS_IMAGE; } void DicomImporter::loadProjections () { DcmDataset* pDataset = m_pFile->getDataset(); Uint16 iNViews = 0, iNDets = 0; if (pDataset->findAndGetUint16 (DCM_Rows, iNViews).bad() || pDataset->findAndGetUint16 (DCM_Columns, iNDets).bad()) { m_bFail = true; m_strFailMessage = "Unable to read header for projections"; return; } DcmElement* pElement = nullptr; const DcmTagKey somatomTag (TAG_GROUP_SOMATOM, TAG_MEMBER_SOMATOM_DATA); if (pDataset->findAndGetElement (somatomTag, pElement).bad() || ! pElement) { m_bFail = true; m_strFailMessage = "Can't find projection data"; return; } Uint8* pRawProjections = nullptr; if (pElement->getUint8Array (pRawProjections).bad() || ! pRawProjections) { m_bFail = true; m_strFailMessage = "Can't read projections data"; return; } const unsigned long lLength = pElement->getLength(); m_iContents = DICOM_CONTENTS_PROJECTIONS; m_pProjections = new Projections; if (! m_pProjections->initFromSomatomAR_STAR (iNViews, iNDets, pRawProjections, lLength)) { m_bFail = true; m_strFailMessage = "Error converting raw projection data"; delete m_pProjections; m_pProjections = nullptr; m_iContents = DICOM_CONTENTS_INVALID; } } // --------------------------------------------------------------------------- // DicomExporter // --------------------------------------------------------------------------- DicomExporter::DicomExporter (ImageFile* pImageFile) : m_pImageFile(pImageFile), m_bFail(false), m_pObject(nullptr) { if (! pImageFile) { m_bFail = true; m_strFailMessage = "Initialized DicomExporter with NULL imagefile"; return; } m_bFail = ! createDicomObject(); } DicomExporter::~DicomExporter () { delete m_pObject; } bool DicomExporter::writeFile (std::string_view svFilename) { if (! m_pObject) return false; m_strFilename = svFilename; OFCondition cond = m_pObject->saveFile (std::string (svFilename).c_str(), EXS_LittleEndianExplicit); if (cond.bad()) { m_bFail = true; m_strFailMessage = "Error writing DICOM file "; m_strFailMessage += svFilename; m_strFailMessage += ": "; m_strFailMessage += cond.text(); return false; } return true; } bool DicomExporter::createDicomObject () { m_pObject = new DcmFileFormat; DcmDataset* pDataset = m_pObject->getDataset(); double dMin, dMax; m_pImageFile->getMinMax (dMin, dMax); double dWidth = dMax - dMin; if (dWidth == 0.) dWidth = 1E-7; const double dScale = 65535. / dWidth; const double dRescaleIntercept = dMin; const double dRescaleSlope = 1 / dScale; char szBuf[128]; char szUID[100]; // the older implementation wrote 1.2.840.10008.5.4.1.1.2 as the SOP class, which // is not a valid UID; CT Image Storage is 1.2.840.10008.5.1.4.1.1.2. pDataset->putAndInsertString (DCM_SOPClassUID, UID_CTImageStorage); dcmGenerateUniqueIdentifier (szUID, SITE_INSTANCE_UID_ROOT); pDataset->putAndInsertString (DCM_SOPInstanceUID, szUID); dcmGenerateUniqueIdentifier (szUID, SITE_STUDY_UID_ROOT); pDataset->putAndInsertString (DCM_StudyInstanceUID, szUID); dcmGenerateUniqueIdentifier (szUID, SITE_SERIES_UID_ROOT); pDataset->putAndInsertString (DCM_SeriesInstanceUID, szUID); dcmGenerateUniqueIdentifier (szUID, SITE_UID_ROOT); pDataset->putAndInsertString (DCM_FrameOfReferenceUID, szUID); pDataset->putAndInsertString (DCM_Modality, "CT"); pDataset->putAndInsertString (DCM_ImageType, "ORIGINAL"); pDataset->putAndInsertString (DCM_Manufacturer, "CTSim"); #ifdef VERSION snprintf (szBuf, sizeof(szBuf), "VERSION %s", VERSION); pDataset->putAndInsertString (DCM_ManufacturerModelName, szBuf); #endif pDataset->putAndInsertString (DCM_PatientName, "NONE"); pDataset->putAndInsertString (DCM_PatientID, "NONE"); pDataset->putAndInsertString (DCM_PatientBirthDate, ""); pDataset->putAndInsertString (DCM_PatientSex, "O"); pDataset->putAndInsertString (DCM_ReferringPhysicianName, "NONE"); pDataset->putAndInsertString (DCM_AccessionNumber, "0"); pDataset->putAndInsertString (DCM_StudyID, "1"); pDataset->putAndInsertString (DCM_SeriesNumber, "1"); pDataset->putAndInsertString (DCM_AcquisitionNumber, "1"); pDataset->putAndInsertString (DCM_InstanceNumber, "1"); pDataset->putAndInsertString (DCM_StudyDate, "00000000"); pDataset->putAndInsertString (DCM_StudyTime, "000000.0"); std::ostringstream osPatComments; m_pImageFile->printLabelsBrief (osPatComments); pDataset->putAndInsertString (DCM_PatientComments, osPatComments.str().c_str()); const double dXInc = m_pImageFile->axisIncrementX(); const double dYInc = m_pImageFile->axisIncrementY(); snprintf (szBuf, sizeof(szBuf), "%e\\%e", dYInc, dXInc); pDataset->putAndInsertString (DCM_PixelSpacing, szBuf); pDataset->putAndInsertString (DCM_ImageOrientationPatient, "1\\0\\0\\0\\1\\0"); pDataset->putAndInsertString (DCM_ImagePositionPatient, "0\\0\\0"); pDataset->putAndInsertString (DCM_PositionReferenceIndicator, "0"); pDataset->putAndInsertString (DCM_KVP, "0"); snprintf (szBuf, sizeof(szBuf), "%e", dXInc); pDataset->putAndInsertString (DCM_SliceThickness, szBuf); const unsigned int nx = m_pImageFile->nx(); const unsigned int ny = m_pImageFile->ny(); pDataset->putAndInsertUint16 (DCM_Rows, static_cast(ny)); pDataset->putAndInsertUint16 (DCM_Columns, static_cast(nx)); pDataset->putAndInsertUint16 (DCM_SamplesPerPixel, 1); pDataset->putAndInsertString (DCM_PhotometricInterpretation, "MONOCHROME2"); pDataset->putAndInsertUint16 (DCM_BitsAllocated, 16); pDataset->putAndInsertUint16 (DCM_BitsStored, 16); pDataset->putAndInsertUint16 (DCM_HighBit, 15); pDataset->putAndInsertUint16 (DCM_PixelRepresentation, 0); snprintf (szBuf, sizeof(szBuf), "%e", dRescaleIntercept); pDataset->putAndInsertString (DCM_RescaleIntercept, szBuf); snprintf (szBuf, sizeof(szBuf), "%e", dRescaleSlope); pDataset->putAndInsertString (DCM_RescaleSlope, szBuf); ImageFileArrayConst v = m_pImageFile->getArray(); std::vector vecPixels (static_cast(nx) * ny); for (unsigned int iy = 0; iy < ny; iy++) for (unsigned int ix = 0; ix < nx; ix++) vecPixels[iy * nx + ix] = static_cast(nearest(dScale * (v[ix][ny - 1 - iy] - dMin))); OFCondition cond = pDataset->putAndInsertUint16Array (DCM_PixelData, vecPixels.data(), vecPixels.size()); if (cond.bad()) { m_bFail = true; m_strFailMessage = "Error storing pixel data: "; m_strFailMessage += cond.text(); return false; } return true; } #endif // HAVE_DCMTK ctsim-7.0.0/libctsim/hdf5file.cpp0000644000175000017500000012105615253666241015666 0ustar kevinkevin/***************************************************************************** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ // HDF5 storage for image and projection files (6.5.0). // // CTSim's own binary formats are fine but nothing else can read them, and the // pixel data was stored as 32-bit float even after the 6.1.0 change that made // memory double. HDF5 fixes both: every intermediate is immediately available // to numpy, MATLAB, ImageJ and HDFView, and all floating point is stored as // double. Everything the old headers carried -- axis extents, scanner // geometry, the history labels -- carries over as attributes and datasets, so // nothing is lost in conversion. // // Layout (format_version 1): // // / attrs: format_version, ctsim_version, content // /image attrs: nx, ny, data_type, axis extents and increments // /image/real double [nx][ny] // /image/imag double [nx][ny] (complex images only) // /projections attrs: n_view, n_det, geometry, det_start, det_inc, // rot_start, rot_inc, focal_length, // source_detector_length, view_diameter, // fan_beam_angle, calc_time, remark // /projections/detvalues double [n_view][n_det] // /projections/view_angles double [n_view] // /history/type int [n] // /history/text variable-length strings [n] // /history/calc_time double [n] // /history/datetime int [n][6] (year, month, day, hour, min, sec) #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "ct.h" #include #include "fileformat.h" #include #include #include #include #include #ifdef HAVE_HDF5 #include #endif // --------------------------------------------------------------------------- // Format sniffing works with or without HDF5 support compiled in, so that a // build without it can still say something useful about a file it cannot read. // --------------------------------------------------------------------------- bool ctsimFileIsHDF5 (std::string_view svFilename) { const std::string strProbe (svFilename); // the C calls below need a terminator static const unsigned char aSignature[8] = { 0x89, 'H', 'D', 'F', '\r', '\n', 0x1a, '\n' }; FILE* fp = fopen (strProbe.c_str(), "rb"); if (! fp) return false; unsigned char aBuf[8] = { 0 }; const size_t nRead = fread (aBuf, 1, sizeof(aBuf), fp); fclose (fp); if (nRead != sizeof(aBuf)) return false; return memcmp (aBuf, aSignature, sizeof(aSignature)) == 0; } // Legacy signatures, as written by Array2dFile and Projections. static const unsigned char aLegacyImageSig[2] = { 'F', 'I' }; // 'I'*256+'F' static const unsigned char aLegacyProjSig[2] = { 'J', 'P' }; // 'P'*256+'J' CTSimFileContent ctsimFileContent (std::string_view svFilename) { const std::string strProbe (svFilename); // the C calls below need a terminator if (ctsimFileIsHDF5 (svFilename)) { #ifdef HAVE_HDF5 hid_t file = H5Fopen (strProbe.c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); if (file < 0) return CTSIM_CONTENT_UNKNOWN; CTSimFileContent content = CTSIM_CONTENT_UNKNOWN; if (H5Aexists (file, "content") > 0) { hid_t attr = H5Aopen (file, "content", H5P_DEFAULT); if (attr >= 0) { hid_t type = H5Aget_type (attr); std::vector vecBuf (H5Tget_size (type) + 1, 0); H5Aread (attr, type, vecBuf.data()); H5Tclose (type); H5Aclose (attr); if (strcmp (vecBuf.data(), "image") == 0) content = CTSIM_CONTENT_IMAGE; else if (strcmp (vecBuf.data(), "projections") == 0) content = CTSIM_CONTENT_PROJECTIONS; else if (strcmp (vecBuf.data(), "plot") == 0) content = CTSIM_CONTENT_PLOT; } } H5Fclose (file); return content; #else return CTSIM_CONTENT_UNKNOWN; #endif } FILE* fp = fopen (strProbe.c_str(), "rb"); if (! fp) return CTSIM_CONTENT_UNKNOWN; unsigned char aBuf[4] = { 0 }; const size_t nRead = fread (aBuf, 1, sizeof(aBuf), fp); fclose (fp); if (nRead != sizeof(aBuf)) return CTSIM_CONTENT_UNKNOWN; if (memcmp (aBuf + 2, aLegacyImageSig, 2) == 0) return CTSIM_CONTENT_IMAGE; if (memcmp (aBuf + 2, aLegacyProjSig, 2) == 0) return CTSIM_CONTENT_PROJECTIONS; // Plot files are text and, from 7.0.0, open with their tag. fp = fopen (strProbe.c_str(), "rb"); if (fp) { char szLine[32] = { 0 }; const size_t n = fread (szLine, 1, sizeof(szLine) - 1, fp); fclose (fp); if (n > 0 && strncmp (szLine, "", 10) == 0) return CTSIM_CONTENT_PLOT; } return CTSIM_CONTENT_UNKNOWN; } static CTSimFileFormat s_defaultFormat = #ifdef HAVE_HDF5 CTSIM_FORMAT_HDF5; #else CTSIM_FORMAT_LEGACY; #endif CTSimFileFormat ctsimDefaultFileFormat () { // Escape hatch: files written by 6.5.0 cannot be read by 6.4 and earlier, so // CTSIM_FILE_FORMAT=legacy restores the old writer without rebuilding. static bool s_bCheckedEnv = false; if (! s_bCheckedEnv) { s_bCheckedEnv = true; if (const char* const pszEnv = getenv ("CTSIM_FILE_FORMAT")) { if (equalsIgnoringCase (pszEnv, "legacy")) s_defaultFormat = CTSIM_FORMAT_LEGACY; else if (equalsIgnoringCase (pszEnv, "hdf5") && ctsimHaveHDF5()) s_defaultFormat = CTSIM_FORMAT_HDF5; } } return s_defaultFormat; } void ctsimSetDefaultFileFormat (CTSimFileFormat format) { #ifndef HAVE_HDF5 if (format == CTSIM_FORMAT_HDF5) return; // silently stay with the only format we have #endif s_defaultFormat = format; } bool ctsimHaveHDF5 () { #ifdef HAVE_HDF5 return true; #else return false; #endif } #ifdef HAVE_HDF5 #include #include "array2dfile.h" #include "imagefile.h" #include "projections.h" static const int CTSIM_HDF5_FORMAT_VERSION = 1; // A file whose major layout this build does not know about is refused rather // than half-read. Within a version the layout only ever grows -- new optional // datasets and attributes -- so an older build reads a newer file of the same // version correctly, simply ignoring what it does not recognize. That is why // the structured history and user attributes added after the first 6.5.0 files // did not need a version bump. A bump is reserved for a change that would make // an old reader produce wrong data rather than merely incomplete data. // --------------------------------------------------------------------------- // Small helpers. The HDF5 C API is verbose; these keep the actual schema // readable below. // --------------------------------------------------------------------------- namespace { class Scalar { // a scalar dataspace that always gets closed public: Scalar() : m_id (H5Screate (H5S_SCALAR)) {} ~Scalar() { if (m_id >= 0) H5Sclose (m_id); } hid_t id() const { return m_id; } private: hid_t m_id; Scalar (const Scalar&); Scalar& operator= (const Scalar&); }; void writeAttrInt (hid_t loc, const char* pszName, int iValue) { Scalar space; hid_t attr = H5Acreate2 (loc, pszName, H5T_NATIVE_INT, space.id(), H5P_DEFAULT, H5P_DEFAULT); if (attr < 0) return; H5Awrite (attr, H5T_NATIVE_INT, &iValue); H5Aclose (attr); } void writeAttrDouble (hid_t loc, const char* pszName, double dValue) { Scalar space; hid_t attr = H5Acreate2 (loc, pszName, H5T_NATIVE_DOUBLE, space.id(), H5P_DEFAULT, H5P_DEFAULT); if (attr < 0) return; H5Awrite (attr, H5T_NATIVE_DOUBLE, &dValue); H5Aclose (attr); } void writeAttrString (hid_t loc, const char* pszName, const std::string& str) { hid_t type = H5Tcopy (H5T_C_S1); H5Tset_size (type, str.length() + 1); H5Tset_strpad (type, H5T_STR_NULLTERM); Scalar space; hid_t attr = H5Acreate2 (loc, pszName, type, space.id(), H5P_DEFAULT, H5P_DEFAULT); if (attr >= 0) { H5Awrite (attr, type, str.c_str()); H5Aclose (attr); } H5Tclose (type); } int readAttrInt (hid_t loc, const char* pszName, int iDefault) { if (H5Aexists (loc, pszName) <= 0) return iDefault; hid_t attr = H5Aopen (loc, pszName, H5P_DEFAULT); if (attr < 0) return iDefault; int iValue = iDefault; H5Aread (attr, H5T_NATIVE_INT, &iValue); H5Aclose (attr); return iValue; } double readAttrDouble (hid_t loc, const char* pszName, double dDefault) { if (H5Aexists (loc, pszName) <= 0) return dDefault; hid_t attr = H5Aopen (loc, pszName, H5P_DEFAULT); if (attr < 0) return dDefault; double dValue = dDefault; H5Aread (attr, H5T_NATIVE_DOUBLE, &dValue); H5Aclose (attr); return dValue; } std::string readAttrString (hid_t loc, const char* pszName) { if (H5Aexists (loc, pszName) <= 0) return std::string(); hid_t attr = H5Aopen (loc, pszName, H5P_DEFAULT); if (attr < 0) return std::string(); hid_t type = H5Aget_type (attr); const size_t nSize = H5Tget_size (type); std::vector vecBuf (nSize + 1, 0); H5Aread (attr, type, vecBuf.data()); H5Tclose (type); H5Aclose (attr); return std::string (vecBuf.data()); } bool writeGrid (hid_t loc, const char* pszName, const std::vector >& vecGrid) { if (vecGrid.empty()) return false; const hsize_t nx = vecGrid.size(); const hsize_t ny = vecGrid[0].size(); hsize_t dims[2] = { nx, ny }; hid_t space = H5Screate_simple (2, dims, nullptr); hid_t dset = H5Dcreate2 (loc, pszName, H5T_IEEE_F64LE, space, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); bool bOK = false; if (dset >= 0) { std::vector vecFlat (static_cast(nx) * ny); for (hsize_t ix = 0; ix < nx; ix++) for (hsize_t iy = 0; iy < ny; iy++) vecFlat[ix * ny + iy] = vecGrid[ix][iy]; bOK = H5Dwrite (dset, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, vecFlat.data()) >= 0; H5Dclose (dset); } H5Sclose (space); return bOK; } bool readGrid (hid_t loc, const char* pszName, std::vector >& vecGrid) { hid_t dset = H5Dopen2 (loc, pszName, H5P_DEFAULT); if (dset < 0) return false; hid_t space = H5Dget_space (dset); hsize_t dims[2] = { 0, 0 }; H5Sget_simple_extent_dims (space, dims, nullptr); std::vector vecFlat (static_cast(dims[0]) * dims[1]); const bool bOK = H5Dread (dset, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, vecFlat.data()) >= 0; H5Sclose (space); H5Dclose (dset); if (! bOK) return false; vecGrid.assign (dims[0], std::vector (dims[1], 0.)); for (hsize_t ix = 0; ix < dims[0]; ix++) for (hsize_t iy = 0; iy < dims[1]; iy++) vecGrid[ix][iy] = vecFlat[ix * dims[1] + iy]; return true; } // The image planes are contiguous (7.0.0~m8), so they are written straight // out: the nested form above had to flatten into a temporary first, which // for a 768 x 768 image meant copying 4.5 MB on every save and every load. bool writeGridFlat (hid_t loc, const char* pszName, const std::vector& vecFlat, unsigned int nx, unsigned int ny) { if (vecFlat.empty()) return false; hsize_t dims[2] = { nx, ny }; hid_t space = H5Screate_simple (2, dims, nullptr); hid_t dset = H5Dcreate2 (loc, pszName, H5T_IEEE_F64LE, space, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); bool bOK = false; if (dset >= 0) { bOK = H5Dwrite (dset, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, vecFlat.data()) >= 0; H5Dclose (dset); } H5Sclose (space); return bOK; } bool readGridFlat (hid_t loc, const char* pszName, std::vector& vecFlat, unsigned int& nx, unsigned int& ny) { hid_t dset = H5Dopen2 (loc, pszName, H5P_DEFAULT); if (dset < 0) return false; hid_t space = H5Dget_space (dset); hsize_t dims[2] = { 0, 0 }; H5Sget_simple_extent_dims (space, dims, nullptr); vecFlat.assign (static_cast (dims[0]) * dims[1], 0.); const bool bOK = H5Dread (dset, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, vecFlat.data()) >= 0; H5Sclose (space); H5Dclose (dset); if (bOK) { nx = static_cast (dims[0]); ny = static_cast (dims[1]); } return bOK; } bool writeVector (hid_t loc, const char* pszName, const std::vector& vec) { if (vec.empty()) return false; hsize_t dim = vec.size(); hid_t space = H5Screate_simple (1, &dim, nullptr); hid_t dset = H5Dcreate2 (loc, pszName, H5T_IEEE_F64LE, space, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); bool bOK = false; if (dset >= 0) { bOK = H5Dwrite (dset, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, vec.data()) >= 0; H5Dclose (dset); } H5Sclose (space); return bOK; } bool readVector (hid_t loc, const char* pszName, std::vector& vec) { hid_t dset = H5Dopen2 (loc, pszName, H5P_DEFAULT); if (dset < 0) return false; hid_t space = H5Dget_space (dset); hsize_t dim = 0; H5Sget_simple_extent_dims (space, &dim, nullptr); vec.assign (dim, 0.); const bool bOK = H5Dread (dset, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, vec.data()) >= 0; H5Sclose (space); H5Dclose (dset); return bOK; } // History labels. Stored as parallel datasets rather than a compound type: // simpler to write, and far simpler for an outside tool to read. void writeLabelVector (hid_t file, const std::vector& vecLabels) { const unsigned int nLabels = vecLabels.size(); if (nLabels == 0) return; hid_t group = H5Gcreate2 (file, "/history", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); if (group < 0) return; std::vector vecType (nLabels); std::vector vecCalcTime (nLabels); std::vector vecDateTime (static_cast(nLabels) * 6); std::vector vecUTCOffset (nLabels); std::vector vecText (nLabels); std::vector vecTextPtr (nLabels); std::vector vecOper (nLabels); std::vector vecOperPtr (nLabels); std::vector vecParams (nLabels); std::vector vecParamsPtr (nLabels); std::vector vecVer (nLabels); std::vector vecVerPtr (nLabels); for (unsigned int i = 0; i < nLabels; i++) { const Array2dFileLabel& rLabel = vecLabels[i]; vecType[i] = rLabel.getLabelType(); vecCalcTime[i] = rLabel.getCalcTime(); int year, month, day, hour, minute, second; rLabel.getDateTime (year, month, day, hour, minute, second); vecDateTime[i * 6 + 0] = year; vecDateTime[i * 6 + 1] = month; vecDateTime[i * 6 + 2] = day; vecDateTime[i * 6 + 3] = hour; vecDateTime[i * 6 + 4] = minute; vecDateTime[i * 6 + 5] = second; vecUTCOffset[i] = rLabel.getUTCOffsetMinutes(); vecText[i] = rLabel.getLabelString(); vecTextPtr[i] = vecText[i].c_str(); vecOper[i] = rLabel.getOperation(); vecOperPtr[i] = vecOper[i].c_str(); vecParams[i] = rLabel.getParameters().encode(); vecParamsPtr[i] = vecParams[i].c_str(); vecVer[i] = rLabel.getVersion(); vecVerPtr[i] = vecVer[i].c_str(); } hsize_t dim = nLabels; { hid_t space = H5Screate_simple (1, &dim, nullptr); hid_t dset = H5Dcreate2 (group, "type", H5T_STD_I32LE, space, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); if (dset >= 0) { H5Dwrite (dset, H5T_NATIVE_INT, H5S_ALL, H5S_ALL, H5P_DEFAULT, vecType.data()); H5Dclose (dset); } H5Sclose (space); } { hid_t space = H5Screate_simple (1, &dim, nullptr); hid_t dset = H5Dcreate2 (group, "calc_time", H5T_IEEE_F64LE, space, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); if (dset >= 0) { H5Dwrite (dset, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, vecCalcTime.data()); H5Dclose (dset); } H5Sclose (space); } { hsize_t dims[2] = { dim, 6 }; hid_t space = H5Screate_simple (2, dims, nullptr); hid_t dset = H5Dcreate2 (group, "datetime", H5T_STD_I32LE, space, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); if (dset >= 0) { H5Dwrite (dset, H5T_NATIVE_INT, H5S_ALL, H5S_ALL, H5P_DEFAULT, vecDateTime.data()); H5Dclose (dset); } H5Sclose (space); } { // Minutes east of UTC for each label's timestamp. hsize_t dims[1] = { dim }; hid_t space = H5Screate_simple (1, dims, nullptr); hid_t dset = H5Dcreate2 (group, "utc_offset_minutes", H5T_STD_I32LE, space, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); if (dset >= 0) { H5Dwrite (dset, H5T_NATIVE_INT, H5S_ALL, H5S_ALL, H5P_DEFAULT, vecUTCOffset.data()); H5Dclose (dset); } H5Sclose (space); } { hid_t type = H5Tcopy (H5T_C_S1); H5Tset_size (type, H5T_VARIABLE); // "text" is the human-readable line; "operation" and "parameters" are the // structured form the text is composed from. Parameters are one // "key=value" per line, so h5dump shows them legibly. const char* const aszName[4] = { "text", "operation", "parameters", "ctsim_version" }; const char** apData[4] = { vecTextPtr.data(), vecOperPtr.data(), vecParamsPtr.data(), vecVerPtr.data() }; for (int iSet = 0; iSet < 4; iSet++) { hid_t space = H5Screate_simple (1, &dim, nullptr); hid_t dset = H5Dcreate2 (group, aszName[iSet], type, space, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); if (dset >= 0) { H5Dwrite (dset, type, H5S_ALL, H5S_ALL, H5P_DEFAULT, apData[iSet]); H5Dclose (dset); } H5Sclose (space); } H5Tclose (type); } H5Gclose (group); } void writeLabels (hid_t file, const Array2dFile& rFile) { std::vector vecLabels; for (unsigned int i = 0; i < rFile.nLabels(); i++) vecLabels.push_back (rFile.labelGet (i)); writeLabelVector (file, vecLabels); } void readLabelVector (hid_t file, std::vector& vecOut) { if (H5Lexists (file, "/history", H5P_DEFAULT) <= 0) return; hid_t group = H5Gopen2 (file, "/history", H5P_DEFAULT); if (group < 0) return; hid_t dsetType = H5Dopen2 (group, "type", H5P_DEFAULT); if (dsetType < 0) { H5Gclose (group); return; } hid_t space = H5Dget_space (dsetType); hsize_t nLabels = 0; H5Sget_simple_extent_dims (space, &nLabels, nullptr); H5Sclose (space); std::vector vecType (nLabels, 0); H5Dread (dsetType, H5T_NATIVE_INT, H5S_ALL, H5S_ALL, H5P_DEFAULT, vecType.data()); H5Dclose (dsetType); std::vector vecCalcTime (nLabels, 0.); if (hid_t d = H5Dopen2 (group, "calc_time", H5P_DEFAULT); d >= 0) { H5Dread (d, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, vecCalcTime.data()); H5Dclose (d); } std::vector vecDateTime (static_cast(nLabels) * 6, 0); if (hid_t d = H5Dopen2 (group, "datetime", H5P_DEFAULT); d >= 0) { H5Dread (d, H5T_NATIVE_INT, H5S_ALL, H5S_ALL, H5P_DEFAULT, vecDateTime.data()); H5Dclose (d); } // Absent in files written before the offset was recorded: those keep the // offset the label was constructed with, which is this machine's. std::vector vecUTCOffset (nLabels, 0); bool bHaveOffsets = false; if (H5Lexists (group, "utc_offset_minutes", H5P_DEFAULT) > 0) { if (hid_t d = H5Dopen2 (group, "utc_offset_minutes", H5P_DEFAULT); d >= 0) { bHaveOffsets = H5Dread (d, H5T_NATIVE_INT, H5S_ALL, H5S_ALL, H5P_DEFAULT, vecUTCOffset.data()) >= 0; H5Dclose (d); } } std::vector vecText (nLabels, nullptr); std::vector vecOper (nLabels, nullptr); std::vector vecParams (nLabels, nullptr); std::vector vecVer (nLabels, nullptr); hid_t typeVar = H5Tcopy (H5T_C_S1); H5Tset_size (typeVar, H5T_VARIABLE); hid_t dsetText = H5Dopen2 (group, "text", H5P_DEFAULT); if (dsetText >= 0) H5Dread (dsetText, typeVar, H5S_ALL, H5S_ALL, H5P_DEFAULT, vecText.data()); hid_t dsetOper = H5Dopen2 (group, "operation", H5P_DEFAULT); if (dsetOper >= 0) H5Dread (dsetOper, typeVar, H5S_ALL, H5S_ALL, H5P_DEFAULT, vecOper.data()); hid_t dsetParams = H5Dopen2 (group, "parameters", H5P_DEFAULT); if (dsetParams >= 0) H5Dread (dsetParams, typeVar, H5S_ALL, H5S_ALL, H5P_DEFAULT, vecParams.data()); hid_t dsetVer = H5Dopen2 (group, "ctsim_version", H5P_DEFAULT); if (dsetVer >= 0) H5Dread (dsetVer, typeVar, H5S_ALL, H5S_ALL, H5P_DEFAULT, vecVer.data()); for (hsize_t i = 0; i < nLabels; i++) { Array2dFileLabel label; label.setLabelType (vecType[i]); label.setCalcTime (vecCalcTime[i]); if (bHaveOffsets) label.setUTCOffsetMinutes (vecUTCOffset[i]); label.setLabelString (vecText[i] ? vecText[i] : ""); if (vecOper[i]) label.setOperation (vecOper[i]); if (vecVer[i]) label.setVersion (vecVer[i]); if (vecParams[i]) { HistoryParams params; params.decode (vecParams[i]); label.setParameters (params); } label.setDateTime (vecDateTime[i * 6 + 0], vecDateTime[i * 6 + 1], vecDateTime[i * 6 + 2], vecDateTime[i * 6 + 3], vecDateTime[i * 6 + 4], vecDateTime[i * 6 + 5]); vecOut.push_back (label); } hid_t aDset[4] = { dsetText, dsetOper, dsetParams, dsetVer }; std::vector* apVec[4] = { &vecText, &vecOper, &vecParams, &vecVer }; for (int iSet = 0; iSet < 4; iSet++) { if (aDset[iSet] >= 0) { hid_t sp = H5Dget_space (aDset[iSet]); H5Dvlen_reclaim (typeVar, sp, H5P_DEFAULT, apVec[iSet]->data()); H5Sclose (sp); H5Dclose (aDset[iSet]); } } H5Tclose (typeVar); H5Gclose (group); } void readLabels (hid_t file, Array2dFile& rFile) { std::vector vecLabels; readLabelVector (file, vecLabels); for (const Array2dFileLabel& rLabel : vecLabels) rFile.labelAdd (rLabel); } // Extensible metadata. Everything under /user is round-tripped verbatim, so a // file written by a later version keeps its extra fields when an older build // reads and rewrites it. void writeUserAttrs (hid_t file, const std::map>& mapUser) { if (mapUser.empty()) return; hid_t group = H5Gcreate2 (file, "/user", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); if (group < 0) return; for (std::map::const_iterator it = mapUser.begin(); it != mapUser.end(); ++it) writeAttrString (group, it->first.c_str(), it->second); H5Gclose (group); } herr_t collectUserAttr (hid_t loc, const char* pszName, const H5A_info_t*, void* pOpData) { std::map* pMap = static_cast*> (pOpData); (*pMap)[pszName] = readAttrString (loc, pszName); return 0; } void readUserAttrs (hid_t file, std::map>& mapUser) { mapUser.clear(); if (H5Lexists (file, "/user", H5P_DEFAULT) <= 0) return; hid_t group = H5Gopen2 (file, "/user", H5P_DEFAULT); if (group < 0) return; hsize_t idx = 0; H5Aiterate2 (group, H5_INDEX_NAME, H5_ITER_INC, &idx, collectUserAttr, &mapUser); H5Gclose (group); } // A list of strings as one variable-length string dataset. static void writeStringList (hid_t loc, const char* pszName, const std::vector& vec) { if (vec.empty()) return; std::vector vecPtr (vec.size()); for (size_t i = 0; i < vec.size(); i++) vecPtr[i] = vec[i].c_str(); hid_t type = H5Tcopy (H5T_C_S1); H5Tset_size (type, H5T_VARIABLE); hsize_t dims[1] = { vec.size() }; hid_t space = H5Screate_simple (1, dims, nullptr); hid_t dset = H5Dcreate2 (loc, pszName, type, space, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); if (dset >= 0) { H5Dwrite (dset, type, H5S_ALL, H5S_ALL, H5P_DEFAULT, vecPtr.data()); H5Dclose (dset); } H5Sclose (space); H5Tclose (type); } static void readStringList (hid_t loc, const char* pszName, std::vector& vec) { vec.clear(); if (H5Lexists (loc, pszName, H5P_DEFAULT) <= 0) return; hid_t dset = H5Dopen2 (loc, pszName, H5P_DEFAULT); if (dset < 0) return; hid_t space = H5Dget_space (dset); hsize_t n = 0; H5Sget_simple_extent_dims (space, &n, nullptr); hid_t type = H5Tcopy (H5T_C_S1); H5Tset_size (type, H5T_VARIABLE); std::vector vecPtr (n, nullptr); if (n > 0 && H5Dread (dset, type, H5S_ALL, H5S_ALL, H5P_DEFAULT, vecPtr.data()) >= 0) { for (hsize_t i = 0; i < n; i++) vec.push_back (vecPtr[i] ? vecPtr[i] : ""); H5Dvlen_reclaim (type, space, H5P_DEFAULT, vecPtr.data()); } H5Tclose (type); H5Sclose (space); H5Dclose (dset); } void writeRootAttrs (hid_t file, const char* pszContent) { writeAttrInt (file, "format_version", CTSIM_HDF5_FORMAT_VERSION); #ifdef VERSION writeAttrString (file, "ctsim_version", VERSION); #endif writeAttrString (file, "content", pszContent); } } // anonymous namespace // --------------------------------------------------------------------------- // Images // --------------------------------------------------------------------------- bool Array2dFile::writeHDF5 (std::string_view filename) { // The HDF5 C API needs a terminated string. const std::string strFilename (filename); hid_t file = H5Fcreate (strFilename.c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT); if (file < 0) return false; writeRootAttrs (file, "image"); hid_t group = H5Gcreate2 (file, "/image", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); if (group < 0) { H5Fclose (file); return false; } writeAttrInt (group, "nx", static_cast(m_nx)); writeAttrInt (group, "ny", static_cast(m_ny)); writeAttrInt (group, "data_type", m_dataType); writeAttrInt (group, "axis_extent_known", m_axisExtentKnown); writeAttrInt (group, "axis_increment_known", m_axisIncrementKnown); writeAttrDouble (group, "min_x", m_minX); writeAttrDouble (group, "max_x", m_maxX); writeAttrDouble (group, "min_y", m_minY); writeAttrDouble (group, "max_y", m_maxY); writeAttrDouble (group, "inc_x", m_axisIncrementX); writeAttrDouble (group, "inc_y", m_axisIncrementY); bool bOK = writeGridFlat (group, "real", m_real, m_nx, m_ny); if (bOK && m_dataType == DATA_TYPE_COMPLEX && ! m_imag.empty()) bOK = writeGridFlat (group, "imag", m_imag, m_nx, m_ny); H5Gclose (group); writeLabels (file, *this); writeUserAttrs (file, m_mapUser); H5Fclose (file); return bOK; } // Returns false and explains itself when the file is too new to read. static bool checkFormatVersion (hid_t file, const char* const svFilename) { const int iVersion = readAttrInt (file, "format_version", 0); if (iVersion == 0) { sys_error (ERR_SEVERE, "{} has no format_version attribute; it was not written by CTSim", svFilename); return false; } if (iVersion > CTSIM_HDF5_FORMAT_VERSION) { sys_error (ERR_SEVERE, "{} is CTSim HDF5 format version {}, but this build understands only version {}. " "Use a newer CTSim, or convert the file with a version that can read it.", svFilename, iVersion, CTSIM_HDF5_FORMAT_VERSION); return false; } return true; } bool Array2dFile::readHDF5 (std::string_view filename) { // The HDF5 C API needs a terminated string. const std::string strFilename (filename); hid_t file = H5Fopen (strFilename.c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); if (file < 0) return false; if (! checkFormatVersion (file, strFilename.c_str())) { H5Fclose (file); return false; } const std::string strContent = readAttrString (file, "content"); if (strContent != "image") { sys_error (ERR_SEVERE, "HDF5 file {} does not contain an image (content={})", strFilename, strContent); H5Fclose (file); return false; } hid_t group = H5Gopen2 (file, "/image", H5P_DEFAULT); if (group < 0) { H5Fclose (file); return false; } m_nx = readAttrInt (group, "nx", 0); m_ny = readAttrInt (group, "ny", 0); m_dataType = readAttrInt (group, "data_type", DATA_TYPE_REAL); m_axisExtentKnown = readAttrInt (group, "axis_extent_known", 0); m_axisIncrementKnown = readAttrInt (group, "axis_increment_known", 0); m_minX = readAttrDouble (group, "min_x", 0.); m_maxX = readAttrDouble (group, "max_x", 0.); m_minY = readAttrDouble (group, "min_y", 0.); m_maxY = readAttrDouble (group, "max_y", 0.); m_axisIncrementX = readAttrDouble (group, "inc_x", 0.); m_axisIncrementY = readAttrDouble (group, "inc_y", 0.); // Both of these go into a legacy file's header and are what its reader // uses to interpret the pixel plane. m_pixelSize = sizeof(kfloat32); m_pixelFormat = PIXEL_FLOAT32; // labelsWrite seeks to m_headersize + m_arraySize, so this must be set // or the labels land on top of the pixel plane. m_arraySize = m_nx * m_ny * m_pixelSize; m_filename = strFilename; unsigned int nxRead = 0, nyRead = 0; bool bOK = readGridFlat (group, "real", m_real, nxRead, nyRead); if (bOK && (nxRead != m_nx || nyRead != m_ny)) { sys_error (ERR_WARNING, "{}: pixel plane is {} x {}, header says {} x {}", strFilename, nxRead, nyRead, m_nx, m_ny); bOK = false; } if (bOK && m_dataType == DATA_TYPE_COMPLEX) { unsigned int nxImag = 0, nyImag = 0; bOK = readGridFlat (group, "imag", m_imag, nxImag, nyImag); } else if (bOK) m_imag.clear(); H5Gclose (group); if (bOK) { readLabels (file, *this); readUserAttrs (file, m_mapUser); } H5Fclose (file); return bOK; } // --------------------------------------------------------------------------- // Projections // --------------------------------------------------------------------------- bool Projections::writeHDF5 (std::string_view filename) { // The HDF5 C API needs a terminated string. const std::string strFilename (filename); hid_t file = H5Fcreate (strFilename.c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT); if (file < 0) return false; writeRootAttrs (file, "projections"); hid_t group = H5Gcreate2 (file, "/projections", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); if (group < 0) { H5Fclose (file); return false; } writeAttrInt (group, "n_view", m_nView); writeAttrInt (group, "n_det", m_nDet); writeAttrInt (group, "geometry", m_geometry); writeAttrDouble (group, "calc_time", m_calcTime); writeAttrDouble (group, "rot_start", m_rotStart); writeAttrDouble (group, "rot_inc", m_rotInc); writeAttrDouble (group, "det_start", m_detStart); writeAttrDouble (group, "det_inc", m_detInc); writeAttrDouble (group, "focal_length", m_dFocalLength); writeAttrDouble (group, "source_detector_length", m_dSourceDetectorLength); writeAttrDouble (group, "view_diameter", m_dViewDiameter); writeAttrDouble (group, "fan_beam_angle", m_dFanBeamAngle); writeAttrString (group, "remark", m_remark); std::vector > vecData (m_nView, std::vector (m_nDet, 0.)); std::vector vecAngles (m_nView, 0.); for (int iv = 0; iv < m_nView; iv++) { const DetectorArray& rDA = getDetectorArray (iv); const std::span spanDet = rDA.detValues(); vecAngles[iv] = rDA.viewAngle(); for (int id = 0; id < m_nDet; id++) vecData[iv][id] = spanDet[id]; } bool bOK = writeGrid (group, "detvalues", vecData); if (bOK) bOK = writeVector (group, "view_angles", vecAngles); writeAttrString (group, "history_operation", m_label.getOperation()); writeAttrString (group, "history_parameters", m_label.getParameters().encode()); writeAttrString (group, "history_ctsim_version", m_label.getVersion()); // The calc time and timestamp were not written at all, so a projection file // read back reported no history calc time and the moment it was reopened. writeAttrDouble (group, "history_calc_time", m_label.getCalcTime()); { int year, month, day, hour, minute, second; m_label.getDateTime (year, month, day, hour, minute, second); writeAttrInt (group, "history_year", year); writeAttrInt (group, "history_month", month); writeAttrInt (group, "history_day", day); writeAttrInt (group, "history_hour", hour); writeAttrInt (group, "history_minute", minute); writeAttrInt (group, "history_second", second); writeAttrInt (group, "history_utc_offset_minutes", m_label.getUTCOffsetMinutes()); } H5Gclose (group); writeUserAttrs (file, m_mapUser); H5Fclose (file); return bOK; } bool Projections::readHDF5 (std::string_view filename) { // The HDF5 C API needs a terminated string. const std::string strFilename (filename); hid_t file = H5Fopen (strFilename.c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); if (file < 0) return false; if (! checkFormatVersion (file, strFilename.c_str())) { H5Fclose (file); return false; } const std::string strContent = readAttrString (file, "content"); if (strContent != "projections") { sys_error (ERR_SEVERE, "HDF5 file {} does not contain projections (content={})", strFilename, strContent); H5Fclose (file); return false; } hid_t group = H5Gopen2 (file, "/projections", H5P_DEFAULT); if (group < 0) { H5Fclose (file); return false; } const int nView = readAttrInt (group, "n_view", 0); const int nDet = readAttrInt (group, "n_det", 0); if (nView <= 0 || nDet <= 0) { H5Gclose (group); H5Fclose (file); return false; } init (nView, nDet); m_geometry = readAttrInt (group, "geometry", Scanner::GEOMETRY_PARALLEL); m_calcTime = readAttrDouble (group, "calc_time", 0.); m_rotStart = readAttrDouble (group, "rot_start", 0.); m_rotInc = readAttrDouble (group, "rot_inc", 0.); m_detStart = readAttrDouble (group, "det_start", 0.); m_detInc = readAttrDouble (group, "det_inc", 0.); m_dFocalLength = readAttrDouble (group, "focal_length", 0.); m_dSourceDetectorLength = readAttrDouble (group, "source_detector_length", 0.); m_dViewDiameter = readAttrDouble (group, "view_diameter", 0.); m_dFanBeamAngle = readAttrDouble (group, "fan_beam_angle", 0.); setRemark (readAttrString (group, "remark")); m_filename = strFilename; std::vector > vecData; std::vector vecAngles; bool bOK = readGrid (group, "detvalues", vecData); if (bOK) readVector (group, "view_angles", vecAngles); if (bOK) { for (int iv = 0; iv < m_nView && iv < static_cast(vecData.size()); iv++) { DetectorArray& rDA = getDetectorArray (iv); const std::span spanDet = rDA.detValues(); if (iv < static_cast(vecAngles.size())) rDA.setViewAngle (vecAngles[iv]); for (int id = 0; id < m_nDet && id < static_cast(vecData[iv].size()); id++) spanDet[id] = vecData[iv][id]; } } { const std::string strOper = readAttrString (group, "history_operation"); if (! strOper.empty()) m_label.setOperation (strOper); HistoryParams params; params.decode (readAttrString (group, "history_parameters")); m_label.setParameters (params); const std::string strVer = readAttrString (group, "history_ctsim_version"); if (! strVer.empty()) m_label.setVersion (strVer); m_label.setCalcTime (readAttrDouble (group, "history_calc_time", m_calcTime)); if (H5Aexists (group, "history_year") > 0) { m_label.setDateTime (readAttrInt (group, "history_year", 0), readAttrInt (group, "history_month", 0), readAttrInt (group, "history_day", 1), readAttrInt (group, "history_hour", 0), readAttrInt (group, "history_minute", 0), readAttrInt (group, "history_second", 0)); if (H5Aexists (group, "history_utc_offset_minutes") > 0) m_label.setUTCOffsetMinutes (readAttrInt (group, "history_utc_offset_minutes", 0)); } } H5Gclose (group); readUserAttrs (file, m_mapUser); H5Fclose (file); return bOK; } // --------------------------------------------------------------------------- // Plot files. Layout: // / content = "plot", ctsim_version, format_version // /plot n_columns, n_records, scatter, date; y_min, y_max and // has_y_range for the display range // /plot/columns n_columns x n_records, one row per column of data // /plot/ezset the plot commands, one string each // /plot/descriptions // /history as for images: the labels of the data this was made from, // then the operation that made it // --------------------------------------------------------------------------- #include "plotfile.h" bool PlotFile::writeHDF5 (std::string_view filename) { const std::string strFilename (filename); // the HDF5 C API needs a terminator hid_t file = H5Fcreate (strFilename.c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT); if (file < 0) { sys_error (ERR_WARNING, "Unable to create {} [PlotFile::writeHDF5]", strFilename); return false; } m_strFilename = strFilename; writeRootAttrs (file, "plot"); hid_t group = H5Gcreate2 (file, "/plot", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); if (group < 0) { H5Fclose (file); return false; } writeAttrInt (group, "n_columns", m_iNumColumns); writeAttrInt (group, "n_records", m_iNumRecords); writeAttrInt (group, "scatter", m_bScatterPlot ? 1 : 0); writeAttrString (group, "date", m_strDate); writeAttrInt (group, "has_y_range", m_bHasYRange ? 1 : 0); writeAttrDouble (group, "y_min", m_bHasYRange ? m_dYMin : 0.); writeAttrDouble (group, "y_max", m_bHasYRange ? m_dYMax : 0.); bool bOK = true; if (m_iNumColumns > 0 && m_iNumRecords > 0) { std::vector > vecGrid (m_iNumColumns, std::vector (m_iNumRecords, 0.)); for (int ic = 0; ic < m_iNumColumns; ic++) for (int ir = 0; ir < m_iNumRecords; ir++) vecGrid[ic][ir] = m_vecCurves[ic * m_iNumRecords + ir]; bOK = writeGrid (group, "columns", vecGrid); } writeStringList (group, "ezset", m_vecStrEzsetCommands); writeStringList (group, "descriptions", m_vecStrDescriptions); H5Gclose (group); writeLabelVector (file, m_vecLabels); H5Fclose (file); return bOK; } bool PlotFile::readHDF5 (std::string_view filename) { const std::string strFilename (filename); // the HDF5 C API needs a terminator hid_t file = H5Fopen (strFilename.c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); if (file < 0) { sys_error (ERR_WARNING, "Unable to open {} [PlotFile::readHDF5]", strFilename); return false; } if (readAttrString (file, "content") != "plot") { sys_error (ERR_WARNING, "{} is not a CTSim plot file [PlotFile::readHDF5]", strFilename); H5Fclose (file); return false; } hid_t group = H5Gopen2 (file, "/plot", H5P_DEFAULT); if (group < 0) { H5Fclose (file); return false; } initHeaders(); m_strFilename = strFilename; const int nColumns = readAttrInt (group, "n_columns", 0); const int nRecords = readAttrInt (group, "n_records", 0); const bool bScatter = readAttrInt (group, "scatter", 0) != 0; m_strDate = readAttrString (group, "date"); if (readAttrInt (group, "has_y_range", 0) != 0) setYRange (readAttrDouble (group, "y_min", 0.), readAttrDouble (group, "y_max", 0.)); else clearYRange(); bool bOK = true; if (nColumns > 0 && nRecords > 0) { setCurveSize (nColumns, nRecords, bScatter); std::vector > vecGrid; bOK = readGrid (group, "columns", vecGrid) && static_cast (vecGrid.size()) == nColumns; if (bOK) for (int ic = 0; ic < nColumns; ic++) addColumn (ic, vecGrid[ic]); // the vector carries its own length } readStringList (group, "ezset", m_vecStrEzsetCommands); readStringList (group, "descriptions", m_vecStrDescriptions); H5Gclose (group); m_vecLabels.clear(); readLabelVector (file, m_vecLabels); H5Fclose (file); return bOK; } #else // ! HAVE_HDF5 #include "array2dfile.h" #include "projections.h" #include "plotfile.h" // Stubs so that callers need no conditional compilation. bool Array2dFile::writeHDF5 (std::string_view) { return false; } bool Array2dFile::readHDF5 (std::string_view) { return false; } bool Projections::writeHDF5 (std::string_view) { return false; } bool Projections::readHDF5 (std::string_view) { return false; } bool PlotFile::writeHDF5 (std::string_view filename) { return fileWriteText (svFilename); } bool PlotFile::readHDF5 (std::string_view) { return false; } #endif // HAVE_HDF5 ctsim-7.0.0/libctsim/fourier.cpp0000644000175000017500000001246715253543770015661 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: fourier.cpp ** Purpose: Fourier transform functions ** Programmer: Kevin Rosenberg ** Date Started: Dec 2000 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #include "ct.h" #include void Fourier::shuffleFourierToNaturalOrder (ImageFile& im) { ImageFileArray vReal = im.getArray(); ImageFileArray vImag = im.getImaginaryArray(); unsigned int ix, iy; unsigned int nx = im.nx(); unsigned int ny = im.ny(); // shuffle each column for (ix = 0; ix < nx; ix++) { Fourier::shuffleFourierToNaturalOrder (vReal[ix], ny); if (im.isComplex()) Fourier::shuffleFourierToNaturalOrder (vImag[ix], ny); } // shuffle each row std::vector pRow (nx); // 6.3.1: was float, truncating double pixels for (iy = 0; iy < ny; iy++) { for (ix = 0; ix < nx; ix++) pRow[ix] = vReal[ix][iy]; Fourier::shuffleFourierToNaturalOrder (pRow.data(), nx); for (ix = 0; ix < nx; ix++) vReal[ix][iy] = pRow[ix]; if (im.isComplex()) { for (ix = 0; ix < nx; ix++) pRow[ix] = vImag[ix][iy]; Fourier::shuffleFourierToNaturalOrder (pRow.data(), nx); for (ix = 0; ix < nx; ix++) vImag[ix][iy] = pRow[ix]; } } } void Fourier::shuffleNaturalToFourierOrder (ImageFile& im) { ImageFileArray vReal = im.getArray(); ImageFileArray vImag = im.getImaginaryArray(); unsigned int ix, iy; unsigned int nx = im.nx(); unsigned int ny = im.ny(); // shuffle each x column for (ix = 0; ix < nx; ix++) { Fourier::shuffleNaturalToFourierOrder (vReal[ix], ny); if (im.isComplex()) Fourier::shuffleNaturalToFourierOrder (vImag[ix], ny); } // shuffle each y row std::vector pRow (nx); // 6.3.1: was float, truncating double pixels for (iy = 0; iy < ny; iy++) { for (ix = 0; ix < nx; ix++) pRow[ix] = vReal[ix][iy]; Fourier::shuffleNaturalToFourierOrder (pRow.data(), nx); for (ix = 0; ix < nx; ix++) vReal[ix][iy] = pRow[ix]; if (im.isComplex()) { for (ix = 0; ix < nx; ix++) pRow[ix] = vImag[ix][iy]; Fourier::shuffleNaturalToFourierOrder (pRow.data(), nx); for (ix = 0; ix < nx; ix++) vImag[ix][iy] = pRow[ix]; } } } #ifdef HAVE_FFTW void Fourier::shuffleNaturalToFourierOrder (fftw_complex* pVector, const int n) { fftw_complex* pTemp = static_cast(fftw_malloc(sizeof(fftw_complex) * n)); int i; if (isOdd(n)) { // Odd int iHalfN = (n - 1) / 2; pTemp[0][0] = pVector[iHalfN][0]; pTemp[0][1] = pVector[iHalfN][1]; for (i = 0; i < iHalfN; i++) { pTemp[i + 1][0] = pVector[i + 1 + iHalfN][0]; pTemp[i + 1][1] = pVector[i + 1 + iHalfN][1]; } for (i = 0; i < iHalfN; i++) { pTemp[i + iHalfN + 1][0] = pVector[i][0]; pTemp[i + iHalfN + 1][1] = pVector[i][1]; } } else { // Even int iHalfN = n / 2; pTemp[0][0] = pVector[iHalfN][0]; pTemp[0][1] = pVector[iHalfN][1]; for (i = 0; i < iHalfN - 1; i++) { pTemp[i + 1][0] = pVector[i + iHalfN + 1][0]; pTemp[i + 1][1] = pVector[i + iHalfN + 1][1]; } for (i = 0; i < iHalfN; i++) { pTemp[i + iHalfN][0] = pVector[i][0]; pTemp[i + iHalfN][1] = pVector[i][1]; } } for (i = 0; i < n; i++) { pVector[i][0] = pTemp[i][0]; pVector[i][1] = pTemp[i][1]; } fftw_free(pTemp); } void Fourier::shuffleFourierToNaturalOrder (fftw_complex* pVector, const int n) { fftw_complex* pTemp = static_cast(fftw_malloc(sizeof(fftw_complex) * n)); int i; if (isOdd(n)) { // Odd int iHalfN = (n - 1) / 2; pTemp[iHalfN][0] = pVector[0][0]; pTemp[iHalfN][1] = pVector[0][1]; for (i = 0; i < iHalfN; i++) { pTemp[i + 1 + iHalfN][0] = pVector[i + 1][0]; pTemp[i + 1 + iHalfN][1] = pVector[i + 1][1]; } for (i = 0; i < iHalfN; i++) { pTemp[i][0] = pVector[i + iHalfN + 1][0]; pTemp[i][1] = pVector[i + iHalfN + 1][1]; } } else { // Even int iHalfN = n / 2; pTemp[iHalfN][0] = pVector[0][0]; pTemp[iHalfN][1] = pVector[0][1]; for (i = 0; i < iHalfN; i++) { pTemp[i][0] = pVector[i + iHalfN][0]; pTemp[i][1] = pVector[i + iHalfN][1]; } for (i = 0; i < iHalfN - 1; i++) { pTemp[i + iHalfN + 1][0] = pVector[i+1][0]; pTemp[i + iHalfN + 1][1] = pVector[i+1][1]; } } for (i = 0; i < n; i++) { pVector[i][0] = pTemp[i][0]; pVector[i][1] = pTemp[i][1]; } fftw_free(pTemp); } #endif ctsim-7.0.0/libctsim/backprojectors.cpp0000644000175000017500000006241015253666241017211 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: backprojectors.cpp Classes for backprojection ** Programmer: Kevin Rosenberg ** Date Started: June 2000 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #include "ct.h" #include #include "interpolator.h" const int Backprojector::BPROJ_INVALID = -1; const int Backprojector::BPROJ_TRIG = 0; const int Backprojector::BPROJ_TABLE = 1; const int Backprojector::BPROJ_DIFF = 2; const int Backprojector::BPROJ_IDIFF = 3; const char* const Backprojector::s_aszBackprojectName[] = { "trig", "table", "diff", "idiff", }; const char* const Backprojector::s_aszBackprojectTitle[] = { "Direct Trigometric", "Trigometric Table", "Difference Iteration", "Integer Difference Iteration", }; const int Backprojector::s_iBackprojectCount = sizeof(s_aszBackprojectName) / sizeof(const char*); const int Backprojector::INTERP_INVALID = -1; const int Backprojector::INTERP_NEAREST = 0; const int Backprojector::INTERP_LINEAR = 1; const int Backprojector::INTERP_CUBIC = 2; const int Backprojector::INTERP_FREQ_PREINTERPOLATION = 3; const char* const Backprojector::s_aszInterpName[] = { "nearest", "linear", "cubic", }; const char* const Backprojector::s_aszInterpTitle[] = { "Nearest", "Linear", "Cubic", }; const int Backprojector::s_iInterpCount = sizeof(s_aszInterpName) / sizeof(const char*); Backprojector::Backprojector (const Projections& proj, ImageFile& im, std::string_view backprojName, std::string_view interpName, const int interpFactor, const ReconstructionROI* pROI) { m_fail = false; m_pBackprojectImplem = nullptr; initBackprojector (proj, im, backprojName, interpName, interpFactor, pROI); } void Backprojector::BackprojectView (const double* const viewData, const double viewAngle) { if (m_pBackprojectImplem != nullptr) m_pBackprojectImplem->BackprojectView (viewData, viewAngle); } void Backprojector::PostProcessing() { if (m_pBackprojectImplem != nullptr) m_pBackprojectImplem->PostProcessing(); } Backprojector::~Backprojector () { delete m_pBackprojectImplem; } // FUNCTION IDENTIFICATION // Backproject* projector = selectBackprojector (...) // // PURPOSE // Selects a backprojector based on BackprojType // and initializes the backprojector bool Backprojector::initBackprojector (const Projections& proj, ImageFile& im, std::string_view backprojName, std::string_view interpName, const int interpFactor, const ReconstructionROI* pROI) { m_nameBackproject = backprojName; m_nameInterpolation = interpName; m_pBackprojectImplem = nullptr; m_idBackproject = convertBackprojectNameToID (backprojName); if (m_idBackproject == BPROJ_INVALID) { m_fail = true; m_failMessage = "Invalid backprojection name "; m_failMessage += backprojName; } m_idInterpolation = convertInterpNameToID (interpName); if (m_idInterpolation == INTERP_INVALID) { m_fail = true; m_failMessage = "Invalid interpolation name "; m_failMessage += interpName; } if (m_fail || m_idBackproject == BPROJ_INVALID || m_idInterpolation == INTERP_INVALID) { m_fail = true; return false; } if (proj.geometry() == Scanner::GEOMETRY_EQUILINEAR) m_pBackprojectImplem = static_cast(new BackprojectEquilinear(proj, im, m_idInterpolation, interpFactor, pROI)); else if (proj.geometry() == Scanner::GEOMETRY_EQUIANGULAR) m_pBackprojectImplem = static_cast(new BackprojectEquiangular(proj, im, m_idInterpolation, interpFactor, pROI)); else if (proj.geometry() == Scanner::GEOMETRY_PARALLEL) { if (m_idBackproject == BPROJ_TRIG) m_pBackprojectImplem = static_cast(new BackprojectTrig (proj, im, m_idInterpolation, interpFactor, pROI)); else if (m_idBackproject == BPROJ_TABLE) m_pBackprojectImplem = static_cast(new BackprojectTable (proj, im, m_idInterpolation, interpFactor, pROI)); else if (m_idBackproject == BPROJ_DIFF) m_pBackprojectImplem = static_cast(new BackprojectDiff (proj, im, m_idInterpolation, interpFactor, pROI)); else if (m_idBackproject == BPROJ_IDIFF) m_pBackprojectImplem = static_cast(new BackprojectIntDiff (proj, im, m_idInterpolation, interpFactor, pROI)); } else { m_fail = true; m_failMessage = "Unable to select a backprojection method [Backprojector::initBackprojector]"; return false; } return true; } int Backprojector::convertBackprojectNameToID (std::string_view name) { int backprojID = BPROJ_INVALID; for (int i = 0; i < s_iBackprojectCount; i++) { if (equalsIgnoringCase (name, s_aszBackprojectName[i])) { backprojID = i; break; } } return (backprojID); } std::string_view Backprojector::convertBackprojectIDToName (int bprojID) { static const char *bprojName = ""; if (bprojID >= 0 && bprojID < s_iBackprojectCount) return (s_aszBackprojectName[bprojID]); return (bprojName); } const char* Backprojector::convertBackprojectIDToTitle (const int bprojID) { static const char *bprojTitle = ""; if (bprojID >= 0 && bprojID < s_iBackprojectCount) return (s_aszBackprojectTitle[bprojID]); return (bprojTitle); } int Backprojector::convertInterpNameToID (std::string_view name) { int interpID = INTERP_INVALID; for (int i = 0; i < s_iInterpCount; i++) { if (equalsIgnoringCase (name, s_aszInterpName[i])) { interpID = i; break; } } return (interpID); } std::string_view Backprojector::convertInterpIDToName (const int interpID) { static const char *interpName = ""; if (interpID >= 0 && interpID < s_iInterpCount) return (s_aszInterpName[interpID]); return (interpName); } const char* Backprojector::convertInterpIDToTitle (const int interpID) { static const char *interpTitle = ""; if (interpID >= 0 && interpID < s_iInterpCount) return (s_aszInterpTitle[interpID]); return (interpTitle); } // CLASS IDENTICATION // Backproject // // PURPOSE // Pure virtual base class for all backprojectors. Backproject::Backproject (const Projections& proj, ImageFile& im, int interpType, const int interpFactor, const ReconstructionROI* pROI) : proj(proj), im(im), interpType(interpType), v(im.getArray()), m_interpFactor(interpFactor), m_bPostProcessingDone(false) { detInc = proj.detInc(); nDet = proj.nDet(); iDetCenter = (nDet - 1) / 2; // index refering to L=0 projection if (proj.geometry() == Scanner::GEOMETRY_PARALLEL) rotScale = PI / proj.nView(); // scale by number of PI rotations else if (proj.geometry() == Scanner::GEOMETRY_EQUIANGULAR || proj.geometry() == Scanner::GEOMETRY_EQUILINEAR) rotScale = (2 * PI) / proj.nView(); // scale by number of 2PI rotations else sys_error (ERR_SEVERE, "Invalid geometry type {} [Backproject::Backproject]", proj.geometry()); nx = im.nx(); ny = im.ny(); im.arrayDataClear(); xMin = -proj.phmLen() / 2; // Retangular coords of phantom xMax = xMin + proj.phmLen(); yMin = -proj.phmLen() / 2; yMax = yMin + proj.phmLen(); if (pROI) { if (pROI->m_dXMin > xMin) xMin = pROI->m_dXMin; if (pROI->m_dXMax < xMax) xMax = pROI->m_dXMax; if (pROI->m_dYMin > yMin) yMin = pROI->m_dYMin; if (pROI->m_dYMax < yMax) yMax = pROI->m_dYMax; if (xMin > xMax) { double temp = xMin; xMin = xMax; xMax = temp; } if (yMin > yMax) { double temp = yMin; yMin = yMax; yMax = temp; } } xInc = (xMax - xMin) / nx; // size of cells yInc = (yMax - yMin) / ny; im.setAxisIncrement (xInc, yInc); im.setAxisExtent (xMin, xMax, yMin, yMax); m_dFocalLength = proj.focalLength(); m_dSourceDetectorLength = proj.sourceDetectorLength(); } Backproject::~Backproject () {} void Backproject::PostProcessing() { m_bPostProcessingDone = true; } void Backproject::ScaleImageByRotIncrement () { #if HAVE_OPENMP #pragma omp parallel for #endif for (int ix = 0; ix < nx; ix++) for (int iy = 0; iy < ny; iy++) v[ix][iy] *= rotScale; } void Backproject::errorIndexOutsideDetector (int ix, int iy, double theta, double r, double phi, double L, int iDetPos) { sys_error (ERR_WARNING, "r={}, phi={}", r, phi); errorIndexOutsideDetector (ix, iy, theta, L, iDetPos); } void Backproject::errorIndexOutsideDetector (int ix, int iy, double theta, double L, int iDetPos) { #if 1 std::ostringstream os; os << "ix=" << ix << ", iy=" << iy << ", theta=" << theta << ", L=" << L << ", detinc=" << detInc << "\n"; os << "ndet=" << nDet << ", detInc=" << detInc << ", iDetCenter=" << iDetCenter << "\n"; os << "xMin=" << xMin << ", xMax=" << xMax << ", xInc=" << xInc << "\n"; os << "yMin=" << yMin << ", yMax=" << yMax << ", yInc=" << yInc << "\n"; os << "iDetPos index outside bounds: " << iDetPos << " [backprojector]";; sys_error (ERR_WARNING, os.str()); #endif } // CLASS IDENTICATION // BackprojectTrig // // PURPOSE // Uses trigometric functions at each point in image for backprojection. void BackprojectTrig::BackprojectView (const double* const filteredProj, const double view_angle) { double theta = view_angle; CubicPolyInterpolator* pCubicInterp = nullptr; if (interpType == Backprojector::INTERP_CUBIC) pCubicInterp = new CubicPolyInterpolator (filteredProj, nDet); double xstart = xMin + xInc / 2; // Rectang coords of center of pixel #if HAVE_OPENMP #pragma omp parallel for #endif for (int ix = 0; ix < nx; ix++) { double x = xstart + (ix * xInc); double y = yMin + yInc / 2; // inner loop: private to this thread, for (int iy = 0; iy < ny; iy++, y += yInc) { // so increment, don't multiply double r = sqrt (x * x + y * y); // distance of cell from center double phi = atan2 (y, x); // angle of cell from center double L = r * cos (theta - phi); // position on detector if (interpType == Backprojector::INTERP_NEAREST) { int iDetPos = iDetCenter + nearest (L / detInc); // calc'd index in the filter raysum array if (iDetPos >= 0 && iDetPos < nDet) v[ix][iy] += rotScale * filteredProj[iDetPos]; } else if (interpType == Backprojector::INTERP_LINEAR) { double p = L / detInc; // position along detector double pFloor = floor (p); int iDetPos = iDetCenter + static_cast(pFloor); double frac = p - pFloor; // fraction distance from det if (iDetPos >= 0 && iDetPos < nDet - 1) v[ix][iy] += rotScale * ((1-frac) * filteredProj[iDetPos] + frac * filteredProj[iDetPos+1]); } else if (interpType == Backprojector::INTERP_CUBIC) { double p = iDetCenter + (L / detInc); // position along detector if (p >= 0 && p < nDet) v[ix][iy] += rotScale * pCubicInterp->interpolate (p); } } } if (interpType == Backprojector::INTERP_CUBIC) delete pCubicInterp; } // CLASS IDENTICATION // BackprojectTable // // PURPOSE // Precalculates trigometric function value for each point in image for backprojection. BackprojectTable::BackprojectTable (const Projections& proj, ImageFile& im, int interpType, const int interpFactor, const ReconstructionROI* pROI) : Backproject (proj, im, interpType, interpFactor, pROI) { arrayR.initSetSize (im.nx(), im.ny()); arrayPhi.initSetSize (im.nx(), im.ny()); r = arrayR.getArray(); phi = arrayPhi.getArray(); double xstart = xMin + xInc / 2; #if HAVE_OPENMP #pragma omp parallel for #endif for (int ix = 0; ix < nx; ix++) { double x = xstart + (ix * xInc); double y = yMin + yInc / 2; for (int iy = 0; iy < ny; iy++, y += yInc) { r[ix][iy] = sqrt (x * x + y * y); phi[ix][iy] = atan2 (y, x); } } } BackprojectTable::~BackprojectTable () { } void BackprojectTable::PostProcessing() { if (! m_bPostProcessingDone) { ScaleImageByRotIncrement(); m_bPostProcessingDone = true; } } void BackprojectTable::BackprojectView (const double* const filteredProj, const double view_angle) { double theta = view_angle; CubicPolyInterpolator* pCubicInterp = nullptr; if (interpType == Backprojector::INTERP_CUBIC) pCubicInterp = new CubicPolyInterpolator (filteredProj, nDet); #if HAVE_OPENMP #pragma omp parallel for #endif for (int ix = 0; ix < nx; ix++) { ImageFileColumn pImCol = v[ix]; for (int iy = 0; iy < ny; iy++) { double L = r[ix][iy] * cos (theta - phi[ix][iy]); if (interpType == Backprojector::INTERP_NEAREST) { int iDetPos = iDetCenter + nearest(L / detInc); // calc index in the filtered raysum vector if (iDetPos >= 0 && iDetPos < nDet) { pImCol[iy] += filteredProj[iDetPos]; } } else if (interpType == Backprojector::INTERP_LINEAR) { double dPos = L / detInc; // position along detector double dPosFloor = floor (dPos); int iDetPos = iDetCenter + static_cast(dPosFloor); double frac = dPos - dPosFloor; // fraction distance from det if (iDetPos >= 0 && iDetPos < nDet - 1) { pImCol[iy] += ((1-frac) * filteredProj[iDetPos] + frac * filteredProj[iDetPos+1]); } } else if (interpType == Backprojector::INTERP_CUBIC) { double p = iDetCenter + (L / detInc); // position along detector if (p >= 0 && p < nDet) { pImCol[iy] += pCubicInterp->interpolate (p); } } } // end for y } // end for x if (interpType == Backprojector::INTERP_CUBIC) delete pCubicInterp; } // CLASS IDENTICATION // BackprojectDiff // // PURPOSE // Backprojects by precalculating the change in L position for each x & y step in the image. // Iterates in x & y direction by adding difference in L position BackprojectDiff::BackprojectDiff (const Projections& proj, ImageFile& im, int interpType, const int interpFactor, const ReconstructionROI* pROI) : Backproject (proj, im, interpType, interpFactor, pROI) { // calculate center of first pixel v[0][0] double x = xMin + xInc / 2; double y = yMin + yInc / 2; start_r = sqrt (x * x + y * y); start_phi = atan2 (y, x); im.arrayDataClear(); } BackprojectDiff::~BackprojectDiff () { } void BackprojectDiff::PostProcessing() { if (! m_bPostProcessingDone) { ScaleImageByRotIncrement(); m_bPostProcessingDone = true; } } void BackprojectDiff::BackprojectView (const double* const filteredProj, const double view_angle) { double theta = view_angle; // Distance between detectors for an angle given in units of detectors double det_dx = xInc * cos (theta) / detInc; double det_dy = yInc * sin (theta) / detInc; // calculate detPosition for first point in image (ix=0, iy=0) double detPosColBase = iDetCenter + start_r * cos (theta - start_phi) / detInc; CubicPolyInterpolator* pCubicInterp = nullptr; double* deltaFilteredProj = nullptr; if (interpType == Backprojector::INTERP_LINEAR) { // The differences for linear interpolation. No OpenMP here: a thousand // subtractions do not repay starting a thread team, and this runs once // per view. The buffer is kept between views rather than reallocated. m_vecDelta.resize (nDet); deltaFilteredProj = m_vecDelta.data(); for (int i = 0; i < nDet - 1; i++) deltaFilteredProj[i] = filteredProj[i+1] - filteredProj[i]; deltaFilteredProj[nDet - 1] = 0; // last detector } else if (interpType == Backprojector::INTERP_CUBIC) { pCubicInterp = new CubicPolyInterpolator (filteredProj, nDet); } int iLastDet = nDet - 1; #if HAVE_OPENMP #pragma omp parallel for #endif for (int ix = 0; ix < nx; ix++) { double detPos = detPosColBase + (ix * det_dx); ImageFileColumn pImCol = v[ix]; // One loop per interpolation, chosen outside: interpType cannot change // during a reconstruction, and testing it per pixel costs a hundred // million tests in a large one. // // The pixel pointer advances on every iy, in range or not. Advancing // only for pixels in range would write every later pixel in a column one // place too low, wherever the reconstructed square reaches beyond what // the detectors cover. switch (interpType) { case Backprojector::INTERP_NEAREST: for (int iy = 0; iy < ny; iy++, detPos += det_dy, pImCol++) { const int iDetPos = nearest (detPos); if (iDetPos >= 0 && iDetPos < nDet) *pImCol += filteredProj[iDetPos]; } break; case Backprojector::INTERP_LINEAR: for (int iy = 0; iy < ny; iy++, detPos += det_dy, pImCol++) { const double detPosFloor = floor (detPos); const int iDetPos = static_cast (detPosFloor); if (iDetPos >= 0 && iDetPos <= iLastDet) { const double frac = detPos - detPosFloor; *pImCol += filteredProj[iDetPos] + frac * deltaFilteredProj[iDetPos]; } } break; case Backprojector::INTERP_CUBIC: for (int iy = 0; iy < ny; iy++, detPos += det_dy, pImCol++) { if (detPos >= 0 && detPos < nDet) *pImCol += pCubicInterp->interpolate (detPos); } break; default: break; } } // end for x if (interpType == Backprojector::INTERP_CUBIC) delete pCubicInterp; // the differences live in m_vecDelta now } // CLASS IDENTICATION // BackprojectIntDiff // // PURPOSE // Highly optimized and integer version of BackprojectDiff void BackprojectIntDiff::BackprojectView (const double* const filteredProj, const double view_angle) { double theta = view_angle; // add half PI to view angle to get perpendicular theta angle // Fixed point with 32 fractional bits, in a type that is 64 bits // everywhere. Sizing the scale from long instead would give Windows, // where it is 32 bits, a coarser scale and a different image from the // same projections. using Fixed = std::int64_t; static constexpr int scaleShift = 32; static constexpr Fixed scale = Fixed (1) << scaleShift; static constexpr Fixed scaleBitmask = scale - 1; static constexpr Fixed halfScale = scale / 2; static const double dInvScale = 1. / scale; const Fixed det_dx = nearest (xInc * cos (theta) / detInc * scale); const Fixed det_dy = nearest (yInc * sin (theta) / detInc * scale); // calculate L for first point in image (0, 0) Fixed detPosColBase = nearest ((start_r * cos (theta - start_phi) / detInc + iDetCenter) * scale); double* deltaFilteredProj = nullptr; CubicPolyInterpolator* pCubicInterp = nullptr; if (interpType == Backprojector::INTERP_LINEAR) { // The scaled differences for linear interpolation. No OpenMP: a // thousand subtractions do not repay starting a thread team, and this // runs once per view. The buffer is kept between views. m_vecDelta.resize (nDet); deltaFilteredProj = m_vecDelta.data(); for (int i = 0; i < nDet - 1; i++) deltaFilteredProj[i] = (filteredProj[i+1] - filteredProj[i]) * dInvScale; deltaFilteredProj[nDet - 1] = 0; // last detector } else if (interpType == Backprojector::INTERP_CUBIC) { pCubicInterp = new CubicPolyInterpolator (filteredProj, nDet); } int iLastDet = nDet - 1; #if HAVE_OPENMP #pragma omp parallel for #endif for (int ix = 0; ix < nx; ix++) { Fixed detPos = detPosColBase + (ix * det_dx); ImageFileColumn pImCol = v[ix]; // One loop per interpolation, chosen outside: interpType is fixed for // the whole reconstruction but was tested on every pixel. switch (interpType) { case Backprojector::INTERP_NEAREST: for (int iy = 0; iy < ny; iy++, detPos += det_dy, pImCol++) { const Fixed iDetPos = (detPos + halfScale) >> scaleShift; if (iDetPos >= 0 && iDetPos <= iLastDet) *pImCol += filteredProj[iDetPos]; } break; case Backprojector::INTERP_FREQ_PREINTERPOLATION: for (int iy = 0; iy < ny; iy++, detPos += det_dy, pImCol++) { const Fixed iDetPos = ((detPos + halfScale) >> scaleShift) * m_interpFactor; if (iDetPos >= 0 && iDetPos <= iLastDet) *pImCol += filteredProj[iDetPos]; } break; case Backprojector::INTERP_LINEAR: for (int iy = 0; iy < ny; iy++, detPos += det_dy, pImCol++) { const Fixed iDetPos = detPos >> scaleShift; if (iDetPos >= 0 && iDetPos <= iLastDet) { const Fixed detRemainder = detPos & scaleBitmask; *pImCol += filteredProj[iDetPos] + (detRemainder * deltaFilteredProj[iDetPos]); } } break; case Backprojector::INTERP_CUBIC: for (int iy = 0; iy < ny; iy++, detPos += det_dy, pImCol++) *pImCol += pCubicInterp->interpolate (static_cast(detPos) / scale); break; default: break; } } // end for ix if (interpType == Backprojector::INTERP_CUBIC) delete pCubicInterp; // the differences live in m_vecDelta now } void BackprojectEquiangular::BackprojectView (const double* const filteredProj, const double view_angle) { double beta = view_angle; CubicPolyInterpolator* pCubicInterp = nullptr; if (interpType == Backprojector::INTERP_CUBIC) pCubicInterp = new CubicPolyInterpolator (filteredProj, nDet); #if HAVE_OPENMP #pragma omp parallel for #endif for (int ix = 0; ix < nx; ix++) { ImageFileColumn pImCol = v[ix]; for (int iy = 0; iy < ny; iy++) { double dAngleDiff = beta - phi[ix][iy]; double rcos_t = r[ix][iy] * cos (dAngleDiff); double rsin_t = r[ix][iy] * sin (dAngleDiff); double dFLPlusSin = m_dFocalLength + rsin_t; double gamma = atan (rcos_t / dFLPlusSin); double dPos = gamma / detInc; // position along detector double dL2 = dFLPlusSin * dFLPlusSin + (rcos_t * rcos_t); if (interpType == Backprojector::INTERP_NEAREST) { int iDetPos = iDetCenter + nearest(dPos); // calc index in the filtered raysum vector if (iDetPos >= 0 && iDetPos < nDet) pImCol[iy] += filteredProj[iDetPos] / dL2; } else if (interpType == Backprojector::INTERP_LINEAR) { double dPosFloor = floor (dPos); int iDetPos = iDetCenter + static_cast(dPosFloor); double frac = dPos - dPosFloor; // fraction distance from det if (iDetPos >= 0 && iDetPos < nDet - 1) pImCol[iy] += (filteredProj[iDetPos] + frac * (filteredProj[iDetPos+1] - filteredProj[iDetPos])) / dL2; } else if (interpType == Backprojector::INTERP_CUBIC) { double d = iDetCenter + dPos; // position along detector if (d >= 0 && d < nDet) pImCol[iy] += pCubicInterp->interpolate (d) / dL2; } } // end for y } // end for x if (interpType == Backprojector::INTERP_CUBIC) delete pCubicInterp; } void BackprojectEquilinear::BackprojectView (const double* const filteredProj, const double view_angle) { double beta = view_angle; CubicPolyInterpolator* pCubicInterp = nullptr; if (interpType == Backprojector::INTERP_CUBIC) pCubicInterp = new CubicPolyInterpolator (filteredProj, nDet); #if HAVE_OPENMP #pragma omp parallel for #endif for (int ix = 0; ix < nx; ix++) { ImageFileColumn pImCol = v[ix]; for (int iy = 0; iy < ny; iy++) { double dAngleDiff = beta - phi[ix][iy]; double rcos_t = r[ix][iy] * cos (dAngleDiff); double rsin_t = r[ix][iy] * sin (dAngleDiff); double dU = (m_dFocalLength + rsin_t) / m_dFocalLength; double dU2 = dU * dU; double dDetPos = rcos_t / dU; // Scale for imaginary detector that passes through origin of phantom, see Kak-Slaney Figure 3.22. dDetPos *= m_dSourceDetectorLength / m_dFocalLength; double dPos = dDetPos / detInc; // position along detector array if (interpType == Backprojector::INTERP_NEAREST) { int iDetPos = iDetCenter + nearest(dPos); // calc index in the filtered raysum vector if (iDetPos >= 0 && iDetPos < nDet) pImCol[iy] += filteredProj[iDetPos] / dU2; } else if (interpType == Backprojector::INTERP_LINEAR) { double dPosFloor = floor (dPos); int iDetPos = iDetCenter + static_cast(dPosFloor); double frac = dPos - dPosFloor; // fraction distance from det if (iDetPos >= 0 && iDetPos < nDet - 1) pImCol[iy] += (filteredProj[iDetPos] + frac * (filteredProj[iDetPos+1] - filteredProj[iDetPos])) / dU2; } else if (interpType == Backprojector::INTERP_CUBIC) { double d = iDetCenter + dPos; // position along detector if (d >= 0 && d < nDet) pImCol[iy] += pCubicInterp->interpolate (d) / dU2; } } // end for y } // end for x if (interpType == Backprojector::INTERP_CUBIC) delete pCubicInterp; } ctsim-7.0.0/libctsim/reconstruct.cpp0000644000175000017500000001652215253543770016555 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: reconstruct.cpp Reconstruction class ** Programmer: Kevin Rosenberg ** Date Started: Aug 84 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #include "ct.h" #include #include /* NAME * Reconstructor::Reconstructor Reconstruct Image from Projections * * SYNOPSIS * im = proj.reconstruct (im, filt_type, filt_param, interp_type) * IMAGE *im Output image * int filt_type Type of convolution filter to use * double filt_param Filter specific parameter * Currently, used only with Hamming filters * int interp_type Type of interpolation method to use * * ALGORITHM * * Calculate one-dimensional filter in spatial domain * Allocate & clear (zero) the 2d output image array * For each projection view * Convolve raysum array with filter * Backproject raysums and add (summate) to image array * end */ Reconstructor::Reconstructor (const Projections& rProj, ImageFile& rIF, std::string_view filterName, double filt_param, std::string_view filterMethodName, const int zeropad, std::string_view filterGenerationName, std::string_view interpName, int interpFactor, std::string_view backprojectName, const int iTrace, ReconstructionROI* pROI, bool bRebinToParallel, SGP* pSGP) : m_rOriginalProj(rProj), m_pProj(bRebinToParallel ? m_rOriginalProj.interpolateToParallel() : &m_rOriginalProj), m_rImagefile(rIF), m_pProcessSignal(0), m_pBackprojector(0), m_iTrace(iTrace), m_bRebinToParallel(bRebinToParallel), m_bFail(false), m_vecPlotXAxis(0) { m_nFilteredProjections = m_pProj->nDet() * interpFactor; double filterBW = 1. / m_pProj->detInc(); m_pProcessSignal = new ProcessSignal (filterName, filterMethodName, filterBW, m_pProj->detInc(), m_pProj->nDet(), filt_param, "spatial", filterGenerationName, zeropad, interpFactor, iTrace, m_pProj->geometry(), m_pProj->focalLength(), m_pProj->sourceDetectorLength(), pSGP); if (m_pProcessSignal->fail()) { m_bFail = true; m_strFailMessage = "Error creating ProcessSignal: "; m_strFailMessage += m_pProcessSignal->failMessage(); delete m_pProcessSignal; m_pProcessSignal = nullptr; return; } m_pBackprojector = new Backprojector (*m_pProj, m_rImagefile, backprojectName, interpName, interpFactor, pROI); if (m_pBackprojector->fail()) { m_bFail = true; m_strFailMessage = "Error creating backprojector: "; m_strFailMessage += m_pBackprojector->failMessage(); delete m_pBackprojector; m_pBackprojector = nullptr; delete m_pProcessSignal; m_pProcessSignal = nullptr; return; } #ifdef HAVE_SGP m_vecPlotXAxis.assign (m_pProj->nDet(), 0.); double x = - ((m_pProj->nDet() - 1) / 2) * m_pProj->detInc(); const double xInc = m_pProj->detInc(); for (int i = 0; i < m_pProj->nDet(); i++, x += xInc) m_vecPlotXAxis[i] = x; #endif } Reconstructor::~Reconstructor () { if (m_bRebinToParallel) delete m_pProj; delete m_pBackprojector; delete m_pProcessSignal; } void Reconstructor::plotFilter (SGP* pSGP) { #ifdef HAVE_SGP int nVecFilter = m_pProcessSignal->getNFilterPoints(); std::vector adPlotXAxis (nVecFilter); if (nVecFilter > 0 && pSGP) { double f = m_pProcessSignal->getFilterMin(); const double filterInc = m_pProcessSignal->getFilterIncrement(); for (int i = 0; i < nVecFilter; i++, f += filterInc) adPlotXAxis[i] = f; if (! m_pProcessSignal->getFilter().empty()) { EZPlot ezplot; ezplot.ezset ("title Filter Response"); ezplot.addCurve (adPlotXAxis, std::span (m_pProcessSignal->getFilter())); ezplot.plot (pSGP); } } #endif } void Reconstructor::reconstructAllViews () { reconstructView (0, m_pProj->nView()); postProcessing(); } void Reconstructor::postProcessing() { m_pBackprojector->PostProcessing(); } void Reconstructor::reconstructView (int iStartView, int iViewCount, SGP* pSGP, bool bBackprojectView, double dGraphWidth) { // A vector rather than new[]: it frees itself on every path out, and it // converts to a span wherever one is wanted. std::vector vecFilteredProj (m_nFilteredProjections); if (iViewCount <= 0) iViewCount = m_pProj->nView() - iStartView; for (int iView = iStartView; iView < (iStartView + iViewCount); iView++) { if (m_iTrace == Trace::TRACE_CONSOLE) std::cout <<"Reconstructing view " << iView << " (last = " << m_pProj->nView() - 1 << ")\n"; const DetectorArray& rDetArray = m_pProj->getDetectorArray (iView); const std::span detval = rDetArray.detValues(); m_pProcessSignal->filterSignal (detval, vecFilteredProj); if (bBackprojectView) m_pBackprojector->BackprojectView (vecFilteredProj.data(), rDetArray.viewAngle()); #ifdef HAVE_SGP if (m_iTrace >= Trace::TRACE_PLOT && pSGP) { EZPlot ezplotProj; std::ostringstream osXLength; osXLength << "xlength " << dGraphWidth; ezplotProj.ezset ("clear"); ezplotProj.ezset ("title Raw Projection"); ezplotProj.ezset ("xticks major 5"); ezplotProj.ezset ("yticks major 5"); ezplotProj.ezset ("xlabel "); ezplotProj.ezset ("ylabel "); ezplotProj.ezset ("yporigin 0.55"); ezplotProj.ezset ("ylength 0.45"); ezplotProj.ezset (osXLength.str().c_str()); ezplotProj.ezset ("box."); ezplotProj.ezset ("grid."); ezplotProj.addCurve (std::span (m_vecPlotXAxis.data(), m_pProj->nDet()), detval); pSGP->setTextPointSize (9); ezplotProj.plot (pSGP); ezplotProj.ezset ("clear"); ezplotProj.ezset ("title Filtered Projection"); ezplotProj.ezset ("xticks major 5"); ezplotProj.ezset ("xlabel "); ezplotProj.ezset ("ylabel "); ezplotProj.ezset ("yticks major 5"); ezplotProj.ezset ("yporigin 0.10"); ezplotProj.ezset ("ylength 0.45"); ezplotProj.ezset (osXLength.str().c_str()); ezplotProj.ezset ("box"); ezplotProj.ezset ("grid"); ezplotProj.addCurve (std::span (m_vecPlotXAxis.data(), m_nFilteredProjections), std::span (vecFilteredProj)); pSGP->setTextPointSize (9); ezplotProj.plot (pSGP); } #endif //HAVE_SGP } } ctsim-7.0.0/libctsim/pngmeta.cpp0000644000175000017500000001120015253543770015621 0ustar kevinkevin/***************************************************************************** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "pngmeta.h" #include #include #include #ifdef HAVE_PNG #include #endif bool pngFileIsPNG (std::string_view svFilename) { const std::string strFile (svFilename); // fopen needs a terminator FILE* fp = fopen (strFile.c_str(), "rb"); if (! fp) return false; unsigned char aSig[8]; const bool bOK = fread (aSig, 1, 8, fp) == 8 && memcmp (aSig, "\x89PNG\r\n\x1a\n", 8) == 0; fclose (fp); return bOK; } bool pngReadMetadata (std::string_view svFilename, PNGMetadata& meta, std::string& strError) { const std::string strFile (svFilename); // fopen needs a terminator meta = PNGMetadata(); #ifndef HAVE_PNG strError = "PNG support is not built in"; return false; #else if (! pngFileIsPNG (svFilename)) { strError = std::string (svFilename) + " is not a PNG file"; return false; } struct stat st; if (stat (strFile.c_str(), &st) == 0) meta.lFileBytes = static_cast (st.st_size); FILE* fp = fopen (strFile.c_str(), "rb"); if (! fp) { strError = std::string ("Unable to open ") + strFile; return false; } png_structp png = png_create_read_struct (PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); png_infop info = png ? png_create_info_struct (png) : nullptr; if (! png || ! info) { if (png) png_destroy_read_struct (&png, info ? &info : nullptr, nullptr); fclose (fp); strError = "libpng could not be initialized"; return false; } if (setjmp (png_jmpbuf (png))) { png_destroy_read_struct (&png, &info, nullptr); fclose (fp); strError = std::string ("libpng failed reading ") + strFile; return false; } png_init_io (png, fp); png_read_info (png, info); // the header and every chunk before the image data png_uint_32 w = 0, h = 0; int iDepth = 0, iColor = 0, iInterlace = 0; png_get_IHDR (png, info, &w, &h, &iDepth, &iColor, &iInterlace, nullptr, nullptr); meta.iWidth = static_cast (w); meta.iHeight = static_cast (h); meta.iBitDepth = iDepth; meta.iChannels = png_get_channels (png, info); meta.bInterlaced = (iInterlace != PNG_INTERLACE_NONE); switch (iColor) { case PNG_COLOR_TYPE_GRAY: meta.strColorType = "grayscale"; break; case PNG_COLOR_TYPE_GRAY_ALPHA: meta.strColorType = "grayscale with alpha"; break; case PNG_COLOR_TYPE_PALETTE: meta.strColorType = "palette"; break; case PNG_COLOR_TYPE_RGB: meta.strColorType = "RGB"; break; case PNG_COLOR_TYPE_RGB_ALPHA: meta.strColorType = "RGB with alpha"; break; default: meta.strColorType = "unknown"; break; } double dGamma = 0.; if (png_get_gAMA (png, info, &dGamma)) { meta.bHasGamma = true; meta.dGamma = dGamma; } png_uint_32 xres = 0, yres = 0; int iUnit = 0; if (png_get_pHYs (png, info, &xres, &yres, &iUnit) && iUnit == PNG_RESOLUTION_METER) { meta.bHasResolution = true; meta.dPixelsPerMetreX = xres; meta.dPixelsPerMetreY = yres; } png_color_16p pBG = nullptr; meta.bHasBackground = png_get_bKGD (png, info, &pBG) != 0; meta.bHasTransparency = png_get_valid (png, info, PNG_INFO_tRNS) != 0; meta.bHasICCProfile = png_get_valid (png, info, PNG_INFO_iCCP) != 0; meta.bHasSRGB = png_get_valid (png, info, PNG_INFO_sRGB) != 0; png_textp pText = nullptr; int nText = 0; if (png_get_text (png, info, &pText, &nText) > 0) for (int i = 0; i < nText; i++) meta.vecText.push_back (std::make_pair (std::string (pText[i].key ? pText[i].key : ""), std::string (pText[i].text ? pText[i].text : ""))); png_destroy_read_struct (&png, &info, nullptr); fclose (fp); return true; #endif } ctsim-7.0.0/libctsim/imagefile.cpp0000644000175000017500000015432615253666241016130 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: imagefile.cpp ** Purpose: Imagefile classes ** Programmer: Kevin Rosenberg ** Date Started: June 2000 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #include "ct.h" #include #include #include #ifdef HAVE_DCMTK #include "dicom.h" #endif #include "interpolator.h" const double ImageFile::s_dRedGrayscaleFactor = 0.299; const double ImageFile::s_dGreenGrayscaleFactor = 0.587; const double ImageFile::s_dBlueGrayscaleFactor = 0.114; const int ImageFile::EXPORT_FORMAT_INVALID = -1; const int ImageFile::EXPORT_FORMAT_TEXT = 0; const int ImageFile::EXPORT_FORMAT_PGM = 1; const int ImageFile::EXPORT_FORMAT_PGMASCII = 2; #ifdef HAVE_PNG const int ImageFile::EXPORT_FORMAT_PNG = 3; const int ImageFile::EXPORT_FORMAT_PNG16 = 4; #endif #ifdef HAVE_DCMTK const int ImageFile::EXPORT_FORMAT_DICOM = 5; #endif const int ImageFile::EXPORT_FORMAT_RAW = 6; const char* ImageFile::s_aszExportFormatName[] = { "text", "pgm", "pgmascii", #ifdef HAVE_PNG "png", "png16", #endif #ifdef HAVE_DCMTK "dicom", #endif }; const char* ImageFile::s_aszExportFormatTitle[] = { "Text", "PGM", "PGM ASCII", #ifdef HAVE_PNG "PNG", "PNG 16-bit", #endif #ifdef HAVE_DCMTK "Dicom", #endif }; const int ImageFile::s_iExportFormatCount = sizeof(s_aszExportFormatName) / sizeof(const char*); const int ImageFile::IMPORT_FORMAT_INVALID = -1; const int ImageFile::IMPORT_FORMAT_PPM = 0; #ifdef HAVE_PNG const int ImageFile::IMPORT_FORMAT_PNG = 1; #endif #ifdef HAVE_DCMTK const int ImageFile::IMPORT_FORMAT_DICOM = 2; #endif const char* ImageFile::s_aszImportFormatName[] = { "ppm", #ifdef HAVE_PNG "png", #endif #ifdef HAVE_DCMTK "dicom", #endif }; const char* ImageFile::s_aszImportFormatTitle[] = { "PPM", #ifdef HAVE_PNG "PNG", #endif #ifdef HAVE_DCMTK "Dicom", #endif }; const int ImageFile::s_iImportFormatCount = sizeof(s_aszImportFormatName) / sizeof(const char*); F32Image::F32Image (int nx, int ny, int dataType) : Array2dFile (nx, ny, sizeof(kfloat32), Array2dFile::PIXEL_FLOAT32, dataType) { } F32Image::F32Image (void) : Array2dFile() { setPixelFormat (Array2dFile::PIXEL_FLOAT32); setPixelSize (sizeof(kfloat32)); setDataType (Array2dFile::DATA_TYPE_REAL); } F64Image::F64Image (int nx, int ny, int dataType) : Array2dFile (nx, ny, sizeof(kfloat64), Array2dFile::PIXEL_FLOAT64, dataType) { } F64Image::F64Image (void) : Array2dFile () { setPixelFormat (PIXEL_FLOAT64); setPixelSize (sizeof(kfloat64)); setDataType (Array2dFile::DATA_TYPE_REAL); } void ImageFile::getCenterCoordinates (unsigned int& iXCenter, unsigned int& iYCenter) { if (isEven (m_nx)) iXCenter = m_nx / 2; else iXCenter = (m_nx - 1) / 2; if (isEven (m_ny)) iYCenter = m_ny / 2; else iYCenter = (m_ny - 1) / 2; } void ImageFile::filterResponse (std::string_view domainName, double bw, std::string_view filterName, double filt_param, double dInputScale, double dOutputScale) { ImageFileArray v = getArray(); const std::string strFilter (filterName), strDomain (domainName); SignalFilter filter (strFilter.c_str(), strDomain.c_str(), bw, filt_param); unsigned int iXCenter, iYCenter; getCenterCoordinates (iXCenter, iYCenter); for (unsigned int ix = 0; ix < m_nx; ix++) for (unsigned int iy = 0; iy < m_ny; iy++) { const std::int64_t dx = static_cast (ix) - iXCenter; const std::int64_t dy = static_cast (iy) - iYCenter; const double r = ::sqrt (static_cast (dx * dx + dy * dy)) * dInputScale; v[ix][iy] = filter.response (r) * dOutputScale; } } // ImageFile::comparativeStatistics Calculate comparative stats // // OUTPUT // d Normalized root mean squared distance measure // r Normalized mean absolute distance measure // e Worst case distance measure // // REFERENCES // G.T. Herman, Image Reconstruction From Projections, 1980 bool ImageFile::comparativeStatistics (const ImageFile& imComp, double& d, double& r, double& e) const { if (imComp.nx() != m_nx && imComp.ny() != m_ny) { sys_error (ERR_WARNING, "Image sizes differ [ImageFile::comparativeStatistics]"); return false; } ImageFileArrayConst v = getArray(); if (m_nx == 0 || m_ny == 0) return false; ImageFileArrayConst vComp = imComp.getArray(); double myMean = 0.; for (unsigned int ix = 0; ix < m_nx; ix++) { for (unsigned int iy = 0; iy < m_ny; iy++) { myMean += v[ix][iy]; } } myMean /= (m_nx * m_ny); double sqErrorSum = 0.; double absErrorSum = 0.; double sqDiffFromMeanSum = 0.; double absValueSum = 0.; for (unsigned int ix2 = 0; ix2 < m_nx; ix2++) { for (unsigned int iy = 0; iy < m_ny; iy++) { double diff = v[ix2][iy] - vComp[ix2][iy]; sqErrorSum += diff * diff; absErrorSum += fabs(diff); double diffFromMean = v[ix2][iy] - myMean; sqDiffFromMeanSum += diffFromMean * diffFromMean; absValueSum += fabs(v[ix2][iy]); } } d = ::sqrt (sqErrorSum / sqDiffFromMeanSum); r = absErrorSum / absValueSum; int hx = m_nx / 2; int hy = m_ny / 2; double eMax = -1; for (int ix3 = 0; ix3 < hx; ix3++) { for (int iy = 0; iy < hy; iy++) { double avgPixel = 0.25 * (v[2*ix3][2*iy] + v[2*ix3+1][2*iy] + v[2*ix3][2*iy+1] + v[2*ix3+1][2*iy+1]); double avgPixelComp = 0.25 * (vComp[2*ix3][2*iy] + vComp[2*ix3+1][2*iy] + vComp[2*ix3][2*iy+1] + vComp[2*ix3+1][2*iy+1]); double error = fabs (avgPixel - avgPixelComp); if (error > eMax) eMax = error; } } e = eMax; return true; } const char* ImageFile::labelOfDistanceMeasure() { return "NormRMS"; } const char* ImageFile::labelOfAbsoluteMeasure() { return "NormMeanAbs"; } const char* ImageFile::labelOfWorstCaseMeasure() { return "Worst2x2"; } const char* ImageFile::nameOfDistanceMeasure() { return "normalized root mean squared distance"; } const char* ImageFile::nameOfAbsoluteMeasure() { return "normalized mean absolute distance"; } const char* ImageFile::nameOfWorstCaseMeasure() { return "worst case 2x2 neighbourhood distance"; } std::string ImageFile::formatComparativeStatistics (double d, double r, double e) { std::ostringstream os; os << std::setprecision (9) << labelOfDistanceMeasure() << "=" << d << ", " << labelOfAbsoluteMeasure() << "=" << r << ", " << labelOfWorstCaseMeasure() << "=" << e; return os.str(); } bool ImageFile::printComparativeStatistics (const ImageFile& imComp, std::ostream& os) const { double d, r, e; if (comparativeStatistics (imComp, d, r, e)) { const std::streamsize iWas = os.precision (9); os << " " << nameOfDistanceMeasure() << " (" << labelOfDistanceMeasure() << "): " << d << std::endl; os << " " << nameOfAbsoluteMeasure() << " (" << labelOfAbsoluteMeasure() << "): " << r << std::endl; os << " " << nameOfWorstCaseMeasure() << " (" << labelOfWorstCaseMeasure() << "): " << e << std::endl; os.precision (iWas); return true; } return false; } void ImageFile::printStatistics (std::ostream& os) const { double min, max, mean, mode, median, stddev; statistics (min, max, mean, mode, median, stddev); if (isComplex()) os << "Real Component Statistics" << std::endl; os << std::setprecision(7); os << " min: " << min << std::endl; os << " max: " << max << std::endl; os << " mean: " << mean << std::endl; os << " mode: " << mode << std::endl; os << "median: " << median << std::endl; os << "stddev: " << stddev << std::endl; if (isComplex()) { statistics (getImaginaryArray(), min, max, mean, mode, median, stddev); os << std::endl << "Imaginary Component Statistics" << std::endl; os << " min: " << min << std::endl; os << " max: " << max << std::endl; os << " mean: " << mean << std::endl; os << " mode: " << mode << std::endl; os << "median: " << median << std::endl; os << "stddev: " << stddev << std::endl; } } void ImageFile::statistics (double& min, double& max, double& mean, double& mode, double& median, double& stddev) const { ImageFileArrayConst v = getArray(); statistics (v, min, max, mean, mode, median, stddev); } void ImageFile::statistics (ImageFileArrayConst v, double& min, double& max, double& mean, double& mode, double& median, double& stddev) const { int nx = m_nx; int ny = m_ny; if (nx == 0 || ny == 0) return; std::vector vecImage; int iVec = 0; vecImage.resize (nx * ny); for (int ix = 0; ix < nx; ix++) { for (int iy = 0; iy < ny; iy++) vecImage[iVec++] = v[ix][iy]; } vectorNumericStatistics (vecImage, nx * ny, min, max, mean, mode, median, stddev); } void ImageFile::getMinMax (double& min, double& max) const { int nx = m_nx; int ny = m_ny; ImageFileArrayConst v = getArray(); if (nx == 0 || ny == 0) return; min = v[0][0]; max = v[0][0]; for (int ix = 0; ix < nx; ix++) { for (int iy = 0; iy < ny; iy++) { if (v[ix][iy] > max) max = v[ix][iy]; if (v[ix][iy] < min) min = v[ix][iy]; } } } bool ImageFile::convertRealToComplex () { if (dataType() != Array2dFile::DATA_TYPE_REAL) return false; if (! reallocRealToComplex()) return false; ImageFileArray vImag = getImaginaryArray(); for (unsigned int ix = 0; ix < m_nx; ix++) { ImageFileColumn vCol = vImag[ix]; for (unsigned int iy = 0; iy < m_ny; iy++) *vCol++ = 0; } return true; } bool ImageFile::convertComplexToReal () { if (dataType() != Array2dFile::DATA_TYPE_COMPLEX) return false; ImageFileArray vReal = getArray(); ImageFileArray vImag = getImaginaryArray(); for (unsigned int ix = 0; ix < m_nx; ix++) { ImageFileColumn vRealCol = vReal[ix]; ImageFileColumn vImagCol = vImag[ix]; for (unsigned int iy = 0; iy < m_ny; iy++) { CTSimComplex c (*vRealCol, *vImagCol); *vRealCol++ = std::abs (c); vImagCol++; } } return reallocComplexToReal(); } bool ImageFile::subtractImages (const ImageFile& rRHS, ImageFile& result) const { if (m_nx != rRHS.nx() || m_ny != rRHS.ny() || m_nx != result.nx() || m_ny != result.ny()) { sys_error (ERR_WARNING, "Difference sizes of images [ImageFile::subtractImage]"); return false; } if (isComplex() || (rRHS.isComplex() && ! result.isComplex())) result.convertRealToComplex(); ImageFileArrayConst vLHS = getArray(); ImageFileArrayConst vLHSImag = getImaginaryArray(); ImageFileArrayConst vRHS = rRHS.getArray(); ImageFileArrayConst vRHSImag = rRHS.getImaginaryArray(); ImageFileArray vResult = result.getArray(); ImageFileArray vResultImag = result.getImaginaryArray(); for (unsigned int ix = 0; ix < m_nx; ix++) { for (unsigned int iy = 0; iy < m_ny; iy++) { vResult[ix][iy] = vLHS[ix][iy] - vRHS[ix][iy]; if (result.isComplex()) { vResultImag[ix][iy] = 0; if (isComplex()) vResultImag[ix][iy] += vLHSImag[ix][iy]; if (rRHS.isComplex()) vResultImag[ix][iy] -= vRHSImag[ix][iy]; } } } return true; } bool ImageFile::addImages (const ImageFile& rRHS, ImageFile& result) const { if (m_nx != rRHS.nx() || m_ny != rRHS.ny() || m_nx != result.nx() || m_ny != result.ny()) { sys_error (ERR_WARNING, "Difference sizes of images [ImageFile::subtractImage]"); return false; } if (isComplex() || (rRHS.isComplex() && ! result.isComplex())) result.convertRealToComplex(); ImageFileArrayConst vLHS = getArray(); ImageFileArrayConst vLHSImag = getImaginaryArray(); ImageFileArrayConst vRHS = rRHS.getArray(); ImageFileArrayConst vRHSImag = rRHS.getImaginaryArray(); ImageFileArray vResult = result.getArray(); ImageFileArray vResultImag = result.getImaginaryArray(); for (unsigned int ix = 0; ix < m_nx; ix++) { for (unsigned int iy = 0; iy < m_ny; iy++) { vResult[ix][iy] = vLHS[ix][iy] + vRHS[ix][iy]; if (result.isComplex()) { vResultImag[ix][iy] = 0; if (isComplex()) vResultImag[ix][iy] += vLHSImag[ix][iy]; if (rRHS.isComplex()) vResultImag[ix][iy] += vRHSImag[ix][iy]; } } } return true; } bool ImageFile::multiplyImages (const ImageFile& rRHS, ImageFile& result) const { if (m_nx != rRHS.nx() || m_ny != rRHS.ny() || m_nx != result.nx() || m_ny != result.ny()) { sys_error (ERR_WARNING, "Difference sizes of images [ImageFile::subtractImage]"); return false; } if (isComplex() || (rRHS.isComplex() && ! result.isComplex())) result.convertRealToComplex(); ImageFileArrayConst vLHS = getArray(); ImageFileArrayConst vLHSImag = getImaginaryArray(); ImageFileArrayConst vRHS = rRHS.getArray(); ImageFileArrayConst vRHSImag = rRHS.getImaginaryArray(); ImageFileArray vResult = result.getArray(); ImageFileArray vResultImag = result.getImaginaryArray(); for (unsigned int ix = 0; ix < m_nx; ix++) { for (unsigned int iy = 0; iy < m_ny; iy++) { if (result.isComplex()) { double dImag = 0; if (isComplex()) dImag = vLHSImag[ix][iy]; std::complex cLHS (vLHS[ix][iy], dImag); dImag = 0; if (rRHS.isComplex()) dImag = vRHSImag[ix][iy]; std::complex cRHS (vRHS[ix][iy], dImag); std::complex cResult = cLHS * cRHS; vResult[ix][iy] = cResult.real(); vResultImag[ix][iy] = cResult.imag(); } else vResult[ix][iy] = vLHS[ix][iy] * vRHS[ix][iy]; } } return true; } bool ImageFile::divideImages (const ImageFile& rRHS, ImageFile& result) const { if (m_nx != rRHS.nx() || m_ny != rRHS.ny() || m_nx != result.nx() || m_ny != result.ny()) { sys_error (ERR_WARNING, "Difference sizes of images [ImageFile::subtractImage]"); return false; } if (isComplex() || (rRHS.isComplex() && ! result.isComplex())) result.convertRealToComplex(); ImageFileArrayConst vLHS = getArray(); ImageFileArrayConst vLHSImag = getImaginaryArray(); ImageFileArrayConst vRHS = rRHS.getArray(); ImageFileArrayConst vRHSImag = rRHS.getImaginaryArray(); ImageFileArray vResult = result.getArray(); ImageFileArray vResultImag = result.getImaginaryArray(); for (unsigned int ix = 0; ix < m_nx; ix++) { for (unsigned int iy = 0; iy < m_ny; iy++) { if (result.isComplex()) { double dImag = 0; if (isComplex()) dImag = vLHSImag[ix][iy]; std::complex cLHS (vLHS[ix][iy], dImag); dImag = 0; if (rRHS.isComplex()) dImag = vRHSImag[ix][iy]; std::complex cRHS (vRHS[ix][iy], dImag); std::complex cResult = cLHS / cRHS; vResult[ix][iy] = cResult.real(); vResultImag[ix][iy] = cResult.imag(); } else { if (! vRHS.empty()) vResult[ix][iy] = vLHS[ix][iy] / vRHS[ix][iy]; else vResult[ix][iy] = 0; } } } return true; } bool ImageFile::invertPixelValues (ImageFile& result) const { if (m_nx != result.nx() || m_ny != result.ny()) { sys_error (ERR_WARNING, "Difference sizes of images [ImageFile::invertPixelValues]"); return false; } if (isComplex() && ! result.isComplex()) result.convertRealToComplex(); ImageFileArrayConst vLHS = getArray(); ImageFileArray vResult = result.getArray(); for (unsigned int ix = 0; ix < m_nx; ix++) { ImageFileColumnConst in = vLHS[ix]; ImageFileColumn out = vResult[ix]; for (unsigned int iy = 0; iy < m_ny; iy++) *out++ = - *in++; } return true; } bool ImageFile::sqrt (ImageFile& result) const { if (m_nx != result.nx() || m_ny != result.ny()) { sys_error (ERR_WARNING, "Difference sizes of images [ImageFile::invertPixelValues]"); return false; } if (isComplex() && ! result.isComplex()) result.convertRealToComplex(); bool bComplexOutput = result.isComplex(); ImageFileArrayConst vLHS = getArray(); if (! bComplexOutput) { // check if should convert to complex output for (unsigned int ix = 0; ix < m_nx; ix++) { for (unsigned int iy = 0; iy < m_ny; iy++) { if (! bComplexOutput && vLHS[ix][iy] < 0) { result.convertRealToComplex(); bComplexOutput = true; break; } } } } ImageFileArrayConst vLHSImag = getImaginaryArray(); ImageFileArray vResult = result.getArray(); ImageFileArray vResultImag = result.getImaginaryArray(); for (unsigned int ix = 0; ix < m_nx; ix++) { for (unsigned int iy = 0; iy < m_ny; iy++) { if (result.isComplex()) { double dImag = 0; if (isComplex()) dImag = vLHSImag[ix][iy]; std::complex cLHS (vLHS[ix][iy], dImag); std::complex cResult = std::sqrt(cLHS); vResult[ix][iy] = cResult.real(); vResultImag[ix][iy] = cResult.imag(); } else vResult[ix][iy] = ::sqrt (vLHS[ix][iy]); } } return true; } bool ImageFile::log (ImageFile& result) const { if (m_nx != result.nx() || m_ny != result.ny()) { sys_error (ERR_WARNING, "Difference sizes of images [ImageFile::log]"); return false; } if (isComplex() && ! result.isComplex()) result.convertRealToComplex(); ImageFileArrayConst vLHS = getArray(); ImageFileArrayConst vLHSImag = getImaginaryArray(); ImageFileArray vResult = result.getArray(); ImageFileArray vResultImag = result.getImaginaryArray(); for (unsigned int ix = 0; ix < m_nx; ix++) { for (unsigned int iy = 0; iy < m_ny; iy++) { if (result.isComplex()) { double dImag = 0; if (isComplex()) dImag = vLHSImag[ix][iy]; std::complex cLHS (vLHS[ix][iy], dImag); std::complex cResult = std::log (cLHS); vResult[ix][iy] = cResult.real(); vResultImag[ix][iy] = cResult.imag(); } else { if (vLHS[ix][iy] > 0) vResult[ix][iy] = ::log (vLHS[ix][iy]); else vResult[ix][iy] = 0; } } } return true; } bool ImageFile::exp (ImageFile& result) const { if (m_nx != result.nx() || m_ny != result.ny()) { sys_error (ERR_WARNING, "Difference sizes of images [ImageFile::invertPixelValues]"); return false; } if (isComplex() && ! result.isComplex()) result.convertRealToComplex(); ImageFileArrayConst vLHS = getArray(); ImageFileArrayConst vLHSImag = getImaginaryArray(); ImageFileArray vResult = result.getArray(); ImageFileArray vResultImag = result.getImaginaryArray(); for (unsigned int ix = 0; ix < m_nx; ix++) { for (unsigned int iy = 0; iy < m_ny; iy++) { if (result.isComplex()) { double dImag = 0; if (isComplex()) dImag = vLHSImag[ix][iy]; std::complex cLHS (vLHS[ix][iy], dImag); std::complex cResult = std::exp (cLHS); vResult[ix][iy] = cResult.real(); vResultImag[ix][iy] = cResult.imag(); } else vResult[ix][iy] = ::exp (vLHS[ix][iy]); } } return true; } bool ImageFile::scaleImage (ImageFile& result) const { unsigned int nx = m_nx; unsigned int ny = m_ny; unsigned int newNX = result.nx(); unsigned int newNY = result.ny(); double dXScale = static_cast(newNX) / static_cast(nx); double dYScale = static_cast(newNY) / static_cast(ny); if (isComplex() && ! result.isComplex()) result.convertRealToComplex(); ImageFileArrayConst vReal = getArray(); ImageFileArrayConst vImag = getImaginaryArray(); ImageFileArray vResult = result.getArray(); ImageFileArray vResultImag = result.getImaginaryArray(); BilinearInterpolator realInterp (vReal, nx, ny); BilinearInterpolator imagInterp (vImag, nx, ny); for (unsigned int ix = 0; ix < newNX; ix++) { for (unsigned int iy = 0; iy < newNY; iy++) { double dXPos = ix / dXScale; double dYPos = iy / dYScale; vResult[ix][iy] = realInterp.interpolate (dXPos, dYPos); if (result.isComplex()) { if (isComplex()) { vResultImag[ix][iy] = imagInterp.interpolate (dXPos, dYPos); } else { vResultImag[ix][iy] = 0; } } } } return true; } #ifdef HAVE_FFTW bool ImageFile::fft (ImageFile& result) const { if (m_nx != result.nx() || m_ny != result.ny()) { sys_error (ERR_WARNING, "Difference sizes of images [ImageFile::invertPixelValues]"); return false; } if (result.dataType() == Array2dFile::DATA_TYPE_REAL) { if (! result.convertRealToComplex ()) return false; } fftw_complex* in = static_cast (fftw_malloc (sizeof(fftw_complex) * m_nx * m_ny)); ImageFileArrayConst vReal = getArray(); ImageFileArrayConst vImag = getImaginaryArray(); unsigned int ix, iy; unsigned int iArray = 0; for (ix = 0; ix < m_nx; ix++) { for (iy = 0; iy < m_ny; iy++) { in[iArray][0] = vReal[ix][iy]; if (isComplex()) in[iArray][1] = vImag[ix][iy]; else in[iArray][1] = 0; iArray++; } } fftw_plan plan = fftw_plan_dft_2d (m_nx, m_ny, in, in, FFTW_FORWARD, FFTW_ESTIMATE); fftw_execute (plan); ImageFileArray vRealResult = result.getArray(); ImageFileArray vImagResult = result.getImaginaryArray(); iArray = 0; unsigned int iScale = m_nx * m_ny; for (ix = 0; ix < m_nx; ix++) { for (iy = 0; iy < m_ny; iy++) { vRealResult[ix][iy] = in[iArray][0] / iScale; vImagResult[ix][iy] = in[iArray][1] / iScale; iArray++; } } fftw_free(in); fftw_destroy_plan (plan); Fourier::shuffleFourierToNaturalOrder (result); return true; } bool ImageFile::ifft (ImageFile& result) const { if (m_nx != result.nx() || m_ny != result.ny()) { sys_error (ERR_WARNING, "Difference sizes of images [ImageFile::invertPixelValues]"); return false; } if (result.dataType() == Array2dFile::DATA_TYPE_REAL) { if (! result.convertRealToComplex ()) return false; } ImageFileArrayConst vReal = getArray(); ImageFileArrayConst vImag = getImaginaryArray(); ImageFileArray vRealResult = result.getArray(); ImageFileArray vImagResult = result.getImaginaryArray(); unsigned int ix, iy; for (ix = 0; ix < m_nx; ix++) { for (iy = 0; iy < m_ny; iy++) { vRealResult[ix][iy] = vReal[ix][iy]; if (isComplex()) vImagResult[ix][iy] = vImag[ix][iy]; else vImagResult[ix][iy] = 0; } } Fourier::shuffleNaturalToFourierOrder (result); fftw_complex* in = static_cast(fftw_malloc(sizeof(fftw_complex) * m_nx * m_ny)); unsigned int iArray = 0; for (ix = 0; ix < m_nx; ix++) { for (iy = 0; iy < m_ny; iy++) { in[iArray][0] = vRealResult[ix][iy]; in[iArray][1] = vImagResult[ix][iy]; iArray++; } } fftw_plan plan = fftw_plan_dft_2d (m_nx, m_ny, in, in, FFTW_BACKWARD, FFTW_ESTIMATE); fftw_execute (plan); iArray = 0; for (ix = 0; ix < m_nx; ix++) { for (iy = 0; iy < m_ny; iy++) { vRealResult[ix][iy] = in[iArray][0]; vImagResult[ix][iy] = in[iArray][1]; iArray++; } } fftw_destroy_plan (plan); fftw_free(in); return true; } bool ImageFile::fftRows () { if (dataType() == Array2dFile::DATA_TYPE_REAL) { if (! convertRealToComplex ()) return false; } ImageFileArray vReal = getArray(); ImageFileArray vImag = getImaginaryArray(); fftw_complex* in = static_cast(fftw_malloc(sizeof(fftw_complex) * m_nx)); fftw_plan plan = fftw_plan_dft_1d (m_nx, in, in, FFTW_FORWARD, FFTW_ESTIMATE); std::vector> pcRow (m_nx); for (unsigned int iy = 0; iy < m_ny; iy++) { unsigned int ix; for (ix = 0; ix < m_nx; ix++) { in[ix][0] = vReal[ix][iy]; if (isComplex()) in[ix][1] = vImag[ix][iy]; else in[ix][1] = 0; } fftw_execute (plan); for (ix = 0; ix < m_nx; ix++) pcRow[ix] = std::complex(in[ix][0], in[ix][1]); Fourier::shuffleFourierToNaturalOrder (pcRow.data(), m_nx); for (ix = 0; ix < m_nx; ix++) { vReal[ix][iy] = pcRow[ix].real() / m_nx; vImag[ix][iy] = pcRow[ix].imag() / m_nx; } } fftw_destroy_plan (plan); fftw_free(in); return true; } bool ImageFile::ifftRows () { if (dataType() == Array2dFile::DATA_TYPE_REAL) { if (! convertRealToComplex ()) return false; } ImageFileArray vReal = getArray(); ImageFileArray vImag = getImaginaryArray(); fftw_complex* in = static_cast(fftw_malloc(sizeof(fftw_complex) * m_nx)); fftw_plan plan = fftw_plan_dft_1d (m_nx, in, in, FFTW_BACKWARD, FFTW_ESTIMATE); std::vector> pcRow (m_nx); unsigned int ix, iy; // unsigned int iArray = 0; for (iy = 0; iy < m_ny; iy++) { for (ix = 0; ix < m_nx; ix++) { double dImag = 0; if (isComplex()) dImag = vImag[ix][iy]; pcRow[ix] = std::complex (vReal[ix][iy], dImag); } Fourier::shuffleNaturalToFourierOrder (pcRow.data(), m_nx); for (ix = 0; ix < m_nx; ix++) { in[ix][0] = pcRow[ix].real(); in[ix][1] = pcRow[ix].imag(); } fftw_execute (plan); for (ix = 0; ix < m_nx; ix++) { vReal[ix][iy] = in[ix][0]; vImag[ix][iy] = in[ix][1]; } } fftw_destroy_plan (plan); fftw_free(in); return true; } bool ImageFile::fftCols () { if (dataType() == Array2dFile::DATA_TYPE_REAL) { if (! convertRealToComplex ()) return false; } ImageFileArray vReal = getArray(); // in-place transform (see getArray() note) ImageFileArray vImag = getImaginaryArray(); fftw_complex* in = static_cast(fftw_malloc(sizeof(fftw_complex) * m_ny)); fftw_plan plan = fftw_plan_dft_1d (m_ny, in, in, FFTW_FORWARD, FFTW_ESTIMATE); std::vector> pcCol (m_ny); for (unsigned int ix = 0; ix < m_nx; ix++) { unsigned int iy; for (iy = 0; iy < m_ny; iy++) { in[iy][0] = vReal[ix][iy]; if (isComplex()) in[iy][1] = vImag[ix][iy]; else in[iy][1] = 0; } fftw_execute (plan); for (iy = 0; iy < m_ny; iy++) pcCol[iy] = std::complex(in[iy][0], in[iy][1]); Fourier::shuffleFourierToNaturalOrder (pcCol.data(), m_ny); for (iy = 0; iy < m_ny; iy++) { vReal[ix][iy] = pcCol[iy].real() / m_ny; vImag[ix][iy] = pcCol[iy].imag() / m_ny; } } fftw_destroy_plan (plan); fftw_free(in); return true; } bool ImageFile::ifftCols () { if (dataType() == Array2dFile::DATA_TYPE_REAL) { if (! convertRealToComplex ()) return false; } ImageFileArray vReal = getArray(); // in-place transform (see getArray() note) ImageFileArray vImag = getImaginaryArray(); fftw_complex* in = static_cast(fftw_malloc(sizeof(fftw_complex) * m_ny)); fftw_plan plan = fftw_plan_dft_1d (m_ny, in, in, FFTW_BACKWARD, FFTW_ESTIMATE); std::vector> pcCol (m_ny); unsigned int ix, iy; // unsigned int iArray = 0; for (ix = 0; ix < m_nx; ix++) { for (iy = 0; iy < m_ny; iy++) { double dImag = 0; if (isComplex()) dImag = vImag[ix][iy]; pcCol[iy] = std::complex (vReal[ix][iy], dImag); } Fourier::shuffleNaturalToFourierOrder (pcCol.data(), m_ny); for (iy = 0; iy < m_ny; iy++) { in[iy][0] = pcCol[iy].real(); in[iy][1] = pcCol[iy].imag(); } fftw_execute (plan); for (iy = 0; iy < m_ny; iy++) { vReal[ix][iy] = in[iy][0]; vImag[ix][iy] = in[iy][1]; } } fftw_destroy_plan (plan); fftw_free(in); return true; } #endif // HAVE_FFTW bool ImageFile::fourier (ImageFile& result) const { if (m_nx != result.nx() || m_ny != result.ny()) { sys_error (ERR_WARNING, "Difference sizes of images [ImageFile::invertPixelValues]"); return false; } if (! result.isComplex()) { if (! result.convertRealToComplex ()) { return false; } } ImageFileArrayConst vLHS = getArray(); ImageFileArrayConst vLHSImag = getImaginaryArray(); ImageFileArray vRealResult = result.getArray(); ImageFileArray vImagResult = result.getImaginaryArray(); unsigned int ix, iy; // alloc output matrix // A vector of rows rather than a table of pointers to separately // allocated rows: complexOut[ix][iy] means the same thing and nothing // has to be freed by hand. std::vector> complexOut (m_nx, std::vector (m_ny)); // fourier each x column std::vector vecpY (m_ny); CTSimComplex* pY = vecpY.data(); for (ix = 0; ix < m_nx; ix++) { for (iy = 0; iy < m_ny; iy++) { double dImag = 0; if (isComplex()) dImag = vLHSImag[ix][iy]; pY[iy] = std::complex(vLHS[ix][iy], dImag); } ProcessSignal::finiteFourierTransform (pY, complexOut[ix].data(), m_ny, ProcessSignal::FORWARD); } // fourier each y row std::vector vecpX (m_nx); CTSimComplex* pX = vecpX.data(); std::vector veccomplexOutRow (m_nx); CTSimComplex* complexOutRow = veccomplexOutRow.data(); for (iy = 0; iy < m_ny; iy++) { for (ix = 0; ix < m_nx; ix++) pX[ix] = complexOut[ix][iy]; ProcessSignal::finiteFourierTransform (pX, complexOutRow, m_nx, ProcessSignal::FORWARD); for (ix = 0; ix < m_nx; ix++) complexOut[ix][iy] = complexOutRow[ix]; } for (ix = 0; ix < m_nx; ix++) for (iy = 0; iy < m_ny; iy++) { vRealResult[ix][iy] = complexOut[ix][iy].real(); vImagResult[ix][iy] = complexOut[ix][iy].imag(); } Fourier::shuffleFourierToNaturalOrder (result); // delete complexOut matrix return true; } bool ImageFile::inverseFourier (ImageFile& result) const { if (m_nx != result.nx() || m_ny != result.ny()) { sys_error (ERR_WARNING, "Difference sizes of images [ImageFile::invertPixelValues]"); return false; } if (result.dataType() == Array2dFile::DATA_TYPE_REAL) { if (! result.convertRealToComplex ()) return false; } ImageFileArrayConst vLHSReal = getArray(); ImageFileArrayConst vLHSImag = getImaginaryArray(); ImageFileArray vRealResult = result.getArray(); ImageFileArray vImagResult = result.getImaginaryArray(); unsigned int ix, iy; // alloc 2d complex output matrix // A vector of rows rather than a table of pointers to separately // allocated rows: complexOut[ix][iy] means the same thing and nothing // has to be freed by hand. std::vector> complexOut (m_nx, std::vector (m_ny)); // put input image into result for (ix = 0; ix < m_nx; ix++) { for (iy = 0; iy < m_ny; iy++) { vRealResult[ix][iy] = vLHSReal[ix][iy]; if (isComplex()) vImagResult[ix][iy] = vLHSImag[ix][iy]; else vImagResult[ix][iy] = 0; } } Fourier::shuffleNaturalToFourierOrder (result); // ifourier each x column std::vector vecpCol (m_ny); CTSimComplex* pCol = vecpCol.data(); for (ix = 0; ix < m_nx; ix++) { for (iy = 0; iy < m_ny; iy++) { pCol[iy] = std::complex (vRealResult[ix][iy], vImagResult[ix][iy]); } ProcessSignal::finiteFourierTransform (pCol, complexOut[ix].data(), m_ny, ProcessSignal::BACKWARD); } // ifourier each y row std::vector veccomplexInRow (m_nx); CTSimComplex* complexInRow = veccomplexInRow.data(); std::vector veccomplexOutRow (m_nx); CTSimComplex* complexOutRow = veccomplexOutRow.data(); for (iy = 0; iy < m_ny; iy++) { for (ix = 0; ix < m_nx; ix++) complexInRow[ix] = complexOut[ix][iy]; ProcessSignal::finiteFourierTransform (complexInRow, complexOutRow, m_nx, ProcessSignal::BACKWARD); for (ix = 0; ix < m_nx; ix++) complexOut[ix][iy] = complexOutRow[ix]; } for (ix = 0; ix < m_nx; ix++) for (iy = 0; iy < m_ny; iy++) { vRealResult[ix][iy] = complexOut[ix][iy].real(); vImagResult[ix][iy] = complexOut[ix][iy].imag(); } // delete complexOut matrix return true; } bool ImageFile::magnitude (ImageFile& result) const { if (m_nx != result.nx() || m_ny != result.ny()) { sys_error (ERR_WARNING, "Difference sizes of images [ImageFile::invertPixelValues]"); return false; } ImageFileArrayConst vReal = getArray(); ImageFileArrayConst vImag = getImaginaryArray(); ImageFileArray vRealResult = result.getArray(); for (unsigned int ix = 0; ix < m_nx; ix++) { for (unsigned int iy = 0; iy < m_ny; iy++) { if (isComplex()) vRealResult[ix][iy] = ::sqrt (vReal[ix][iy] * vReal[ix][iy] + vImag[ix][iy] * vImag[ix][iy]); else vRealResult[ix][iy] = ::fabs(vReal[ix][iy]); } } if (result.isComplex()) result.reallocComplexToReal(); return true; } bool ImageFile::phase (ImageFile& result) const { if (m_nx != result.nx() || m_ny != result.ny()) { sys_error (ERR_WARNING, "Difference sizes of images [ImageFile::invertPixelValues]"); return false; } ImageFileArrayConst vReal = getArray(); ImageFileArrayConst vImag = getImaginaryArray(); ImageFileArray vRealResult = result.getArray(); for (unsigned int ix = 0; ix < m_nx; ix++) { for (unsigned int iy = 0; iy < m_ny; iy++) { if (isComplex()) vRealResult[ix][iy] = ::atan2 (vImag[ix][iy], vReal[ix][iy]); else vRealResult[ix][iy] = 0; } } if (result.isComplex()) result.reallocComplexToReal(); return true; } bool ImageFile::real (ImageFile& result) const { if (m_nx != result.nx() || m_ny != result.ny()) { sys_error (ERR_WARNING, "Difference sizes of images [ImageFile::invertPixelValues]"); return false; } ImageFileArrayConst vReal = getArray(); ImageFileArray vRealResult = result.getArray(); for (unsigned int ix = 0; ix < m_nx; ix++) { for (unsigned int iy = 0; iy < m_ny; iy++) { vRealResult[ix][iy] = vReal[ix][iy]; } } if (result.isComplex()) result.reallocComplexToReal(); return true; } bool ImageFile::imaginary (ImageFile& result) const { if (m_nx != result.nx() || m_ny != result.ny()) { sys_error (ERR_WARNING, "Difference sizes of images [ImageFile::invertPixelValues]"); return false; } ImageFileArrayConst vImag = getArray(); ImageFileArray vRealResult = result.getArray(); for (unsigned int ix = 0; ix < m_nx; ix++) { for (unsigned int iy = 0; iy < m_ny; iy++) { if (isComplex()) vRealResult[ix][iy] = vImag[ix][iy]; else vRealResult[ix][iy] = 0; } } if (result.isComplex()) result.reallocComplexToReal(); return true; } bool ImageFile::square (ImageFile& result) const { if (m_nx != result.nx() || m_ny != result.ny()) { sys_error (ERR_WARNING, "Difference sizes of images [ImageFile::invertPixelValues]"); return false; } if (isComplex() && ! result.isComplex()) result.convertRealToComplex(); ImageFileArrayConst vLHS = getArray(); ImageFileArrayConst vLHSImag = getImaginaryArray(); ImageFileArray vResult = result.getArray(); ImageFileArray vResultImag = result.getImaginaryArray(); for (unsigned int ix = 0; ix < m_nx; ix++) { for (unsigned int iy = 0; iy < m_ny; iy++) { if (result.isComplex()) { double dImag = 0; if (isComplex()) dImag = vLHSImag[ix][iy]; std::complex cLHS (vLHS[ix][iy], dImag); std::complex cResult = cLHS * cLHS; vResult[ix][iy] = cResult.real(); vResultImag[ix][iy] = cResult.imag(); } else vResult[ix][iy] = vLHS[ix][iy] * vLHS[ix][iy]; } } return true; } int ImageFile::convertExportFormatNameToID (std::string_view name) { int formatID = EXPORT_FORMAT_INVALID; for (int i = 0; i < s_iExportFormatCount; i++) { if (!equalsIgnoringCase (name, s_aszExportFormatName[i]) == 0) { formatID = i; break; } } return (formatID); } const char* ImageFile::convertExportFormatIDToName (int formatID) { static const char *formatName = ""; if (formatID >= 0 && formatID < s_iExportFormatCount) { return (s_aszExportFormatName[formatID]); } return (formatName); } const char* ImageFile::convertExportFormatIDToTitle (const int formatID) { static const char *formatTitle = ""; if (formatID >= 0 && formatID < s_iExportFormatCount) return (s_aszExportFormatTitle[formatID]); return (formatTitle); } int ImageFile::convertImportFormatNameToID (std::string_view name) { int formatID = IMPORT_FORMAT_INVALID; for (int i = 0; i < s_iImportFormatCount; i++) { if (!equalsIgnoringCase (name, s_aszImportFormatName[i]) == 0) { formatID = i; break; } } return (formatID); } const char* ImageFile::convertImportFormatIDToName (int formatID) { static const char *formatName = ""; if (formatID >= 0 && formatID < s_iImportFormatCount) return (s_aszImportFormatName[formatID]); return (formatName); } const char* ImageFile::convertImportFormatIDToTitle (const int formatID) { static const char *formatTitle = ""; if (formatID >= 0 && formatID < s_iImportFormatCount) return (s_aszImportFormatTitle[formatID]); return (formatTitle); } bool ImageFile::importImage (std::string_view format, std::string_view filename) { // fopen and the image libraries below need a terminated string. const std::string strPath (filename); int iFormatID = convertImportFormatNameToID (format); if (iFormatID == IMPORT_FORMAT_PPM) return readImagePPM (filename); #ifdef HAVE_PNG else if (iFormatID == IMPORT_FORMAT_PNG) return readImagePNG (filename); #endif sys_error (ERR_SEVERE, "Invalid format {} [ImageFile::importImage]", format); return false; } // Reads one non-negative decimal integer, as fscanf("%d") did, but without // its two failings: it cannot overflow silently, and it distinguishes // end-of-file from a missing number. A file from outside the program gets // checked rather than trusted. bool ImageFile::readIntPPM (FILE* fp, int& iValue) { int c = fgetc (fp); if (! isdigit (c)) { if (c != EOF) ungetc (c, fp); return false; } // int64_t, not long: this guards against a header claiming billions of // rows, and it can only do that in a type wider than the int it is // guarding. long is wider on Linux and macOS but not on Windows, where // the accumulation would have overflowed before the check saw it. std::int64_t lValue = 0; while (isdigit (c)) { lValue = lValue * 10 + (c - '0'); if (lValue > std::numeric_limits::max()) return false; c = fgetc (fp); } if (c != EOF) ungetc (c, fp); iValue = static_cast (lValue); return true; } void ImageFile::skipSpacePPM (FILE* fp) { // EOF is tested at every read: without it a file whose last line is a // comment, with no newline after it, never leaves the inner loop. int c = fgetc (fp); while (c != EOF && (isspace (c) || c == '#')) { if (c == '#') { // comment until end of line c = fgetc (fp); while (c != EOF && c != 13 && c != 10) c = fgetc (fp); } else c = fgetc (fp); } if (c != EOF) ungetc (c, fp); } bool ImageFile::readImagePPM (std::string_view filename) { // fopen and the image libraries below need a terminated string. const std::string strPath (filename); FILE* fp = fopen (strPath.c_str(), "r"); if ((fp = fopen (strPath.c_str(), "r")) == nullptr) return false; char cSignature = toupper(fgetc(fp)); if (cSignature != 'P') { fclose(fp); return false; } cSignature = fgetc(fp); if (cSignature == '5' || cSignature == '6') { // binary modes fclose(fp); fp = fopen (strPath.c_str(), "rb"); // reopen in binary mode fgetc(fp); fgetc(fp); } else if (cSignature != '2' && cSignature != '3') { fclose(fp); return false; } int nRows, nCols, iMaxValue; skipSpacePPM (fp); if (! readIntPPM (fp, nCols)) { fclose(fp); return false; } skipSpacePPM (fp); if (! readIntPPM (fp, nRows)) { fclose(fp); return false; } skipSpacePPM (fp); if (! readIntPPM (fp, iMaxValue)) { fclose(fp); return false; } // A PGM/PPM header is data from outside the program: nothing downstream // survives zero or negative dimensions, and iMaxValue divides every pixel. if (nCols <= 0 || nRows <= 0 || iMaxValue <= 0) { sys_error (ERR_WARNING, "Invalid PGM/PPM header in {}: {} x {}, maxval {}", filename, nCols, nRows, iMaxValue); fclose (fp); return false; } setArraySize (nRows, nCols); if (cSignature == '5' || cSignature == '6') { // binary modes int c = fgetc(fp); if (c == 13) { c = fgetc(fp); if (c != 10) // read msdos 13-10 newline ungetc(c, fp); } } else skipSpacePPM (fp); // ascii may have comments bool bMonochromeImage = false; double dMaxValue = iMaxValue; double dMaxValue3 = iMaxValue * 3; ImageFileArray v = getArray(); for (int iy = nRows - 1; iy >= 0; iy--) { for (int ix = 0; ix < nCols; ix++) { int iGS, iR, iG, iB; double dR, dG, dB; switch (cSignature) { case '2': skipSpacePPM (fp); if (! readIntPPM (fp, iGS)) { fclose(fp); return false; } v[ix][iy] = iGS / dMaxValue; break; case '5': iGS = fgetc(fp); if (iGS == EOF) { fclose(fp); return false; } v[ix][iy] = iGS / dMaxValue; break; case '3': skipSpacePPM (fp); if (! readIntPPM (fp, iR)) { fclose(fp); return false; } skipSpacePPM (fp); if (! readIntPPM (fp, iG)) { fclose(fp); return false; } skipSpacePPM (fp); if (! readIntPPM (fp, iB)) { fclose(fp); return false; } if (ix == 0 && iy == 0 && (iR == iG && iG == iB)) bMonochromeImage = true; if (bMonochromeImage) v[ix][iy] = (iR + iG + iB) / dMaxValue3; else { dR = iR / dMaxValue; dG = iG / dMaxValue; dB = iB / dMaxValue; v[ix][iy] = colorToGrayscale (dR, dG, dB); } break; case '6': iR = fgetc(fp); iG = fgetc(fp); iB = fgetc(fp); if (iB == EOF) { fclose(fp); return false; } if (ix == 0 && iy == 0 && (iR == iG && iG == iB)) bMonochromeImage = true; if (bMonochromeImage) v[ix][iy] = (iR + iG + iB) / dMaxValue3; else { dR = iR / dMaxValue; dG = iG / dMaxValue; dB = iB / dMaxValue; v[ix][iy] = colorToGrayscale (dR, dG, dB); } break; } } } fclose(fp); return true; } #ifdef HAVE_PNG bool ImageFile::readImagePNG (std::string_view filename) { // fopen and the image libraries below need a terminated string. const std::string strPath (filename); FILE* fp = fopen (strPath.c_str(), "rb"); if (!fp) return false; unsigned char header[8]; int n = fread (header, 1, 8, fp); if (n != 8 || png_sig_cmp (header, 0, 8)) { fclose (fp); return false; } png_structp png_ptr = png_create_read_struct (PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); if (!png_ptr) { fclose(fp); return false; } png_infop info_ptr = png_create_info_struct(png_ptr); if (!info_ptr) { png_destroy_read_struct(&png_ptr, (png_infopp)nullptr, (png_infopp)nullptr); fclose(fp); return false; } png_infop end_info = png_create_info_struct(png_ptr); if (!end_info) { png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)nullptr); fclose(fp); return false; } if (setjmp(png_jmpbuf(png_ptr))) { png_destroy_read_struct(&png_ptr, &info_ptr, &end_info); fclose(fp); return false; } png_init_io(png_ptr, fp); png_set_sig_bytes(png_ptr, 8); png_read_info(png_ptr, info_ptr); int width = png_get_image_width (png_ptr, info_ptr); int height = png_get_image_height (png_ptr, info_ptr); int bit_depth = png_get_bit_depth (png_ptr, info_ptr); int color_type = png_get_color_type (png_ptr, info_ptr); if (color_type == PNG_COLOR_TYPE_PALETTE && bit_depth <= 8) png_set_expand(png_ptr); if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) png_set_expand(png_ptr); if (bit_depth < 8) png_set_packing(png_ptr); // Transparency: composite onto black rather than discard the alpha // channel. A transparent pixel is "nothing there", which for an image is // black and for a phantom is attenuation 0; stripping alpha instead kept // whatever colour the transparent pixels carried, so a phantom drawn on a // transparent background came out with a background of that colour. A // tRNS chunk on a palette or grey image is transparency too, so it is // expanded to alpha first. if (png_get_valid (png_ptr, info_ptr, PNG_INFO_tRNS)) png_set_tRNS_to_alpha (png_ptr); if ((color_type & PNG_COLOR_MASK_ALPHA) || png_get_valid (png_ptr, info_ptr, PNG_INFO_tRNS)) { png_color_16 black; black.index = 0; black.red = black.green = black.blue = black.gray = 0; png_set_background (png_ptr, &black, PNG_BACKGROUND_GAMMA_SCREEN, 0, 1.0); } if (bit_depth == 16) png_set_swap(png_ptr); // convert to little-endian format png_read_update_info(png_ptr, info_ptr); // update with transformations int rowbytes = png_get_rowbytes (png_ptr, info_ptr); bit_depth = png_get_bit_depth (png_ptr, info_ptr); color_type = png_get_color_type (png_ptr, info_ptr); png_bytep* row_pointers = new png_bytep [height]; int i; for (i = 0; i < height; i++) row_pointers[i] = new unsigned char [rowbytes]; png_read_image(png_ptr, row_pointers); setArraySize (width, height); ImageFileArray v = getArray(); for (int iy = 0; iy < height; iy++) { for (int ix = 0; ix < width; ix++) { double dV = 0; if (color_type == PNG_COLOR_TYPE_GRAY) { if (bit_depth == 8) dV = row_pointers[iy][ix] / 255.; else if (bit_depth == 16) { int iBase = ix * 2; // 65535, not 65536. writeImagePNG scales by (1< rowp (nx * nxcell); if ((fp = fopen (strPath.c_str(), "wb")) == nullptr) return false; fprintf(fp, "P5\n"); fprintf(fp, "%d %d\n", nx, ny); fprintf(fp, "255\n"); for (int irow = ny - 1; irow >= 0; irow--) { for (int icol = 0; icol < nx; icol++) { int pos = icol * nxcell; double dens = (v[icol][irow] - densmin) / (densmax - densmin); dens = clamp (dens, 0., 1.); for (int p = pos; p < pos + nxcell; p++) { rowp[p] = static_cast (dens * 255.); } } for (int ir = 0; ir < nycell; ir++) { for (int ic = 0; ic < nx * nxcell; ic++) fputc( rowp[ic], fp ); } } fclose(fp); return true; } bool ImageFile::writeImagePGMASCII (std::string_view outfile, int nxcell, int nycell, double densmin, double densmax) { // fopen and the image libraries below need a terminated string. const std::string strPath (outfile); FILE *fp; int nx = m_nx; int ny = m_ny; ImageFileArray v = getArray(); std::vector rowp (nx * nxcell); if ((fp = fopen (strPath.c_str(), "wb")) == nullptr) return false; fprintf(fp, "P2\n"); fprintf(fp, "%d %d\n", nx, ny); fprintf(fp, "255\n"); for (int irow = ny - 1; irow >= 0; irow--) { for (int icol = 0; icol < nx; icol++) { int pos = icol * nxcell; double dens = (v[icol][irow] - densmin) / (densmax - densmin); dens = clamp (dens, 0., 1.); for (int p = pos; p < pos + nxcell; p++) { rowp[p] = static_cast (dens * 255.); } } for (int ir = 0; ir < nycell; ir++) { for (int ic = 0; ic < nx * nxcell; ic++) fprintf(fp, "%d ", rowp[ic]); fprintf(fp, "\n"); } } fclose(fp); return true; } bool ImageFile::writeImageText (std::string_view outfile) { // fopen and the image libraries below need a terminated string. const std::string strPath (outfile); FILE *fp; int nx = m_nx; int ny = m_ny; ImageFileArray v = getArray(); ImageFileArray vImag = getImaginaryArray(); if ((fp = fopen (strPath.c_str(), "w")) == nullptr) return false; for (int irow = ny - 1; irow >= 0; irow--) { for (int icol = 0; icol < nx; icol++) { if (isComplex()) { if (vImag[icol][irow] >= 0) fprintf (fp, "%.9g+%.9gi ", v[icol][irow], vImag[icol][irow]); else fprintf (fp, "%.9g-%.9gi ", v[icol][irow], -vImag[icol][irow]); } else fprintf (fp, "%12.8g ", v[icol][irow]); } fprintf(fp, "\n"); } fclose(fp); return true; } #ifdef HAVE_PNG bool ImageFile::writeImagePNG (std::string_view outfile, int bitdepth, int nxcell, int nycell, double densmin, double densmax) { // fopen and the image libraries below need a terminated string. const std::string strPath (outfile); double max_out_level = (1 << bitdepth) - 1; int nx = m_nx; int ny = m_ny; ImageFileArray v = getArray(); std::vector rowp (nx * nxcell * (bitdepth / 8)); FILE *fp = fopen (strPath.c_str(), "wb"); if (! fp) return false; png_structp png_ptr = png_create_write_struct (PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); if (! png_ptr) return false; png_infop info_ptr = png_create_info_struct (png_ptr); if (! info_ptr) { png_destroy_write_struct (&png_ptr, (png_infopp) nullptr); fclose (fp); return false; } if (setjmp(png_jmpbuf(png_ptr))) { png_destroy_write_struct (&png_ptr, &info_ptr); fclose (fp); return false; } png_init_io(png_ptr, fp); png_set_IHDR (png_ptr, info_ptr, nx * nxcell, ny * nycell, bitdepth, PNG_COLOR_TYPE_GRAY, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); png_write_info(png_ptr, info_ptr); #ifndef WORDS_BIGENDIAN if (bitdepth == 16) png_set_swap (png_ptr); // PNG requires big-endian 16-bit samples #endif for (int irow = ny - 1; irow >= 0; irow--) { png_bytep row_pointer = rowp.data(); for (int icol = 0; icol < nx; icol++) { int pos = icol * nxcell; double dens = (v[icol][irow] - densmin) / (densmax - densmin); dens = clamp (dens, 0., 1.); unsigned int outval = static_cast (dens * max_out_level); for (int p = pos; p < pos + nxcell; p++) { if (bitdepth == 8) rowp[p] = outval; else { int rowpos = p * 2; rowp[rowpos+1] = (outval >> 8) & 0xFF; rowp[rowpos] = (outval & 0xFF); } } } for (int ir = 0; ir < nycell; ir++) png_write_rows (png_ptr, &row_pointer, 1); } png_write_end (png_ptr, info_ptr); png_destroy_write_struct (&png_ptr, &info_ptr); fclose(fp); return true; } #endif bool ImageFile::writeImageRaw (std::string_view outfile, int nxcell, int nycell) { // fopen and the image libraries below need a terminated string. const std::string strPath (outfile); FILE *fp; int nx = m_nx; int ny = m_ny; ImageFileArray v = getArray(); if ((fp = fopen (strPath.c_str(), "wb")) == nullptr) return false; for (int irow = ny - 1; irow >= 0; irow--) { for (int icol = 0; icol < nx; icol++) { float dens = v[icol][irow]; fwrite(&dens, sizeof(float), 1, fp); } } fclose(fp); return true; } ctsim-7.0.0/libctsim/array2dfile.cpp0000644000175000017500000005447715253666241016420 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: array2dfile.cpp ** Purpose: 2-dimension array file class ** Programmer: Kevin Rosenberg ** Date Started: June 2000 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #include "array2dfile.h" #include "fileformat.h" #include #ifdef MSVC #endif #include const kuint16 Array2dFile::m_signature = ('I'*256+'F'); /////////////////////////////////////////////////////////////////////////// // CLASS IMPLEMENTATION // // Name: Array2dFileLabel // Purpose: Labels for Array2dFiles /////////////////////////////////////////////////////////////////////////// void Array2dFileLabel::init (void) { m_calcTime = 0; m_labelType = L_EMPTY; time_t t = time(0); tm* lt = localtime(&t); m_year = lt->tm_year; m_month = lt->tm_mon; m_day = lt->tm_mday; m_hour = lt->tm_hour; m_minute = lt->tm_min; m_second = lt->tm_sec; // How far this local time is from UTC, so the stamp is unambiguous. // tm_gmtoff is seconds east of UTC on the platforms CTSim builds on. #if defined(__unix__) || defined(__APPLE__) m_iUTCOffsetMinutes = static_cast (lt->tm_gmtoff / 60); #else { tm gm = *gmtime (&t); const time_t tGM = mktime (&gm); m_iUTCOffsetMinutes = static_cast (difftime (t, tGM) / 60); } #endif } Array2dFileLabel::Array2dFileLabel() { init(); } Array2dFileLabel::Array2dFileLabel (std::string_view str, double ctime) : m_strLabel (str) { init(); m_labelType = L_USER; m_calcTime = ctime; } Array2dFileLabel::Array2dFileLabel (const int type, std::string_view str, double ctime) : m_strLabel (str) { init(); m_labelType = type; m_calcTime = ctime; } Array2dFileLabel::~Array2dFileLabel() { } void Array2dFileLabel::setDateTime (int year, int month, int day, int hour, int minute, int second) { m_year = year; m_month = month; m_day = day; m_hour = hour; m_minute = minute; m_second = second; } void Array2dFileLabel::getDateTime (int& year, int& month, int& day, int& hour, int& minute, int& second) const { year = m_year; month = m_month; day = m_day; hour = m_hour; minute = m_minute; second = m_second; } const std::string& Array2dFileLabel::getDateString (void) const { // ISO-like, with the offset from UTC: "2026-09-12 19:14:02 +02:00". const int iOffset = m_iUTCOffsetMinutes; const char cSign = (iOffset < 0) ? '-' : '+'; const int iAbs = (iOffset < 0) ? -iOffset : iOffset; char szDate [128]; snprintf (szDate, sizeof(szDate), "%4d-%02d-%02d %02d:%02d:%02d %c%02d:%02d", m_year + 1900, m_month + 1, m_day, m_hour, m_minute, m_second, cSign, iAbs / 60, iAbs % 60); m_strDate = szDate; return m_strDate; } Array2dFileLabel::Array2dFileLabel (const Array2dFileLabel& rhs) { m_calcTime = rhs.m_calcTime; m_labelType = rhs.m_labelType; m_strLabel = rhs.m_strLabel; m_iUTCOffsetMinutes = rhs.m_iUTCOffsetMinutes; m_year = rhs.m_year; m_month = rhs.m_month; m_day = rhs.m_day; m_hour = rhs.m_hour; m_minute = rhs.m_minute; m_second = rhs.m_second; m_strOperation = rhs.m_strOperation; // structured history m_strVersion = rhs.m_strVersion; m_params = rhs.m_params; } Array2dFileLabel& Array2dFileLabel::operator= (const Array2dFileLabel& rhs) { m_calcTime = rhs.m_calcTime; m_labelType = rhs.m_labelType; m_strLabel = rhs.m_strLabel; m_iUTCOffsetMinutes = rhs.m_iUTCOffsetMinutes; m_year = rhs.m_year; m_month = rhs.m_month; m_day = rhs.m_day; m_hour = rhs.m_hour; m_minute = rhs.m_minute; m_second = rhs.m_second; m_strOperation = rhs.m_strOperation; // structured history m_strVersion = rhs.m_strVersion; m_params = rhs.m_params; return (*this); } void Array2dFileLabel::print (std::ostream& os) const { if (m_labelType == L_HISTORY) { os << "History: " << std::endl; os << " " << m_strLabel << std::endl; os << " calc time = " << m_calcTime << " secs" << std::endl; os << " Timestamp = " << getDateString() << std::endl; } else if (m_labelType == L_USER) { os << "Note: " << m_strLabel << std::endl; os << " Timestamp = %s" << getDateString() << std::endl; } else { os << "Unknown (" << m_labelType << "): " << m_strLabel << std::endl; os << " Timestamp = %s" << getDateString() << std::endl; } } void Array2dFileLabel::printBrief (std::ostream& os) const { if (m_labelType == L_HISTORY) { os << "History ("; if (m_calcTime > 0) os << m_calcTime << " secs, "; os << getDateString() << "): " << m_strLabel << std::endl; } else if (m_labelType == L_USER) { os << "Note (" << getDateString() << "): " << m_strLabel << std::endl; } else { os << "Unknown (" << getDateString() << "): " << m_strLabel << std::endl; } } void Array2dFileLabel::printBrief (std::ostringstream& os) const { if (m_labelType == L_HISTORY) { os << "History ("; if (m_calcTime > 0) os << m_calcTime << " secs, "; os << getDateString().c_str() << "): " << m_strLabel.c_str() << "\n"; } else if (m_labelType == L_USER) { os << "Note (" << getDateString() << "): " << m_strLabel << "\n"; } else { os << "Unknown (" << getDateString() << "): " << m_strLabel << "\n"; } } /////////////////////////////////////////////////////////////////////////// // CLASS IMPLEMENTATION // // Name: Array2dFile // Purpose: Array2dFiles /////////////////////////////////////////////////////////////////////////// Array2dFile::Array2dFile (int x, int y, int pixelSize, int pixelFormat, int dataType) { init(); setArraySize (x, y, pixelSize, pixelFormat, dataType); } Array2dFile::~Array2dFile (void) { for (labelIterator l = m_labels.begin(); l != m_labels.end(); l++) delete *l; } Array2dFile::Array2dFile (void) { init(); } void Array2dFile::init (void) { m_pixelSize = 0; m_pixelFormat = PIXEL_INVALID; m_dataType = DATA_TYPE_INVALID; m_nx = 0; m_ny = 0; m_headersize = 0; m_axisIncrementKnown = false; m_axisIncrementX = m_axisIncrementY = 0; m_axisExtentKnown = false; m_minX = m_maxX = m_minY = m_maxY = 0; m_offsetPV = 0; m_scalePV = 1; } void Array2dFile::setArraySize (int x, int y, int pixelSize, int pixelFormat, int dataType) { m_pixelSize = pixelSize; m_pixelFormat = pixelFormat; m_dataType = dataType; setArraySize (x, y); } void Array2dFile::setArraySize (int x, int y) { m_nx = x; m_ny = y; allocArrays (); } bool Array2dFile::reallocComplexToReal () { if (m_dataType != DATA_TYPE_COMPLEX) return false; m_imag.clear(); m_dataType = DATA_TYPE_REAL; return true; } bool Array2dFile::reallocRealToComplex () { if (m_dataType != DATA_TYPE_REAL) return false; m_imag.assign (static_cast (m_nx) * m_ny, 0.); m_dataType = DATA_TYPE_COMPLEX; return true; } void Array2dFile::allocArrays () { m_arraySize = m_nx * m_ny * m_pixelSize; m_real.assign (static_cast (m_nx) * m_ny, 0.); if (m_dataType == DATA_TYPE_COMPLEX) m_imag.assign (static_cast (m_nx) * m_ny, 0.); else m_imag.clear(); } void Array2dFile::freeArrays () { m_real.clear(); m_imag.clear(); } bool Array2dFile::fileWrite (std::string_view filename) { // HDF5 unless the caller asked for the legacy format. if (ctsimDefaultFileFormat() == CTSIM_FORMAT_HDF5 && ctsimHaveHDF5()) return writeHDF5 (filename); m_filename = filename; frnetorderstream fs (m_filename.c_str(), std::ios::out | std::ios::in | std::ios::trunc | std::ios::binary); if (fs.fail()) { sys_error (ERR_WARNING, "Error opening file {} for writing [fileCreate]", m_filename); return false; } if (! headerWrite(fs)) return false; if (! arrayDataWrite (fs)) return false; if (! labelsWrite (fs)) return false; return true; } bool Array2dFile::fileRead (std::string_view filename) { // the format is detected from the file, so old and new both open. const std::string strFilename (filename); // the C API below needs a terminator if (ctsimFileIsHDF5 (strFilename.c_str())) { if (! ctsimHaveHDF5()) { sys_error (ERR_SEVERE, "{} is an HDF5 file but this build has no HDF5 support", filename); return false; } return readHDF5 (filename); } m_filename = filename; frnetorderstream fs (m_filename.c_str(), std::ios::out | std::ios::in | std::ios::binary); if (fs.fail()) { sys_error (ERR_WARNING, "Unable to open file {} [fileRead]", m_filename); return false; } if (! headerRead(fs)) return false; allocArrays (); if (! arrayDataRead(fs)) return false;; if (! labelsRead (fs)) return false; return true; } void Array2dFile::setAxisIncrement (double incX, double incY) { m_axisIncrementKnown = true; m_axisIncrementX = incX; m_axisIncrementY = incY; } void Array2dFile::setAxisExtent (double minX, double maxX, double minY, double maxY) { m_axisExtentKnown = true; m_minX = minX; m_maxX = maxX; m_minY = minY; m_maxY = maxY; } bool Array2dFile::headerRead (frnetorderstream& fs) { if (! fs) { sys_error (ERR_WARNING, "Tried to read header with file closed [headerRead]"); return false; } fs.seekg (0); kuint16 file_signature; fs.readInt16 (m_headersize); fs.readInt16 (file_signature); fs.readInt16 (m_pixelFormat); fs.readInt16 (m_pixelSize); fs.readInt16 (m_numFileLabels); fs.readInt32 (m_nx); fs.readInt32 (m_ny); fs.readInt16 (m_dataType); fs.readInt16 (m_axisIncrementKnown); fs.readFloat64 (m_axisIncrementX); fs.readFloat64 (m_axisIncrementY); fs.readInt16 (m_axisExtentKnown); fs.readFloat64 (m_minX); fs.readFloat64 (m_maxX); fs.readFloat64 (m_minY); fs.readFloat64 (m_maxY); fs.readFloat64 (m_offsetPV); fs.readFloat64 (m_scalePV); int read_m_headersize = fs.tellg(); if (read_m_headersize != m_headersize) { sys_error (ERR_WARNING, "Read m_headersize {} != file m_headersize {}", read_m_headersize, m_headersize); return false; } if (file_signature != m_signature) { sys_error (ERR_WARNING, "File signature {} != true signature {}", file_signature, m_signature); return false; } return ! fs.fail(); } bool Array2dFile::headerWrite (frnetorderstream& fs) { if (! fs) { sys_error (ERR_WARNING, "Tried to write header with ! fs"); return false; } m_numFileLabels = m_labels.size(); fs.seekp (0); fs.writeInt16 (m_headersize); fs.writeInt16 (m_signature); fs.writeInt16 (m_pixelFormat); fs.writeInt16 (m_pixelSize); fs.writeInt16 (m_numFileLabels); fs.writeInt32 (m_nx); fs.writeInt32 (m_ny); fs.writeInt16 (m_dataType); fs.writeInt16 (m_axisIncrementKnown); fs.writeFloat64 (m_axisIncrementX); fs.writeFloat64 (m_axisIncrementY); fs.writeInt16 (m_axisExtentKnown); fs.writeFloat64 (m_minX); fs.writeFloat64 (m_maxX); fs.writeFloat64 (m_minY); fs.writeFloat64 (m_maxY); fs.writeFloat64 (m_offsetPV); fs.writeFloat64 (m_scalePV); m_headersize = static_cast(fs.tellp()); fs.seekp (0); fs.writeInt16 (m_headersize); return ! fs.fail(); } // pixel storage is double in memory; the file format is unchanged. // These helpers translate one column between memory and m_pixelSize-byte // samples (4 = float32, the format written by every CTSim release; 8 = float64). template static void columnToDisk (const double* src, size_t n, std::vector& dst) { dst.resize (n); for (size_t i = 0; i < n; i++) dst[i] = static_cast(src[i]); } template static void columnFromDisk (const std::vector& src, double* dst) { for (size_t i = 0; i < src.size(); i++) dst[i] = static_cast(src[i]); } template static bool writeColumns (frnetorderstream& fs, const std::vector& data, unsigned int nx, unsigned int ny, int pixelSize) { std::vector col; const int columnSize = ny * pixelSize; for (unsigned int ix = 0; ix < nx; ix++) { columnToDisk (data.data() + static_cast (ix) * ny, ny, col); unsigned char* ptr = reinterpret_cast(col.data()); if (NativeBigEndian()) { for (unsigned int iy = 0; iy < ny; iy++) { ConvertReverseNetworkOrder (ptr, pixelSize); fs.write (reinterpret_cast(ptr), pixelSize); ptr += pixelSize; } } else fs.write (reinterpret_cast(col.data()), columnSize); } return true; } template static bool readColumns (frnetorderstream& fs, std::vector& data, unsigned int nx, unsigned int ny, int pixelSize) { std::vector col (ny); const int columnSize = ny * pixelSize; for (unsigned int ix = 0; ix < nx; ix++) { unsigned char* ptr = reinterpret_cast(col.data()); if (NativeBigEndian()) { for (unsigned int iy = 0; iy < ny; iy++) { fs.read (reinterpret_cast(ptr), pixelSize); ConvertReverseNetworkOrder (ptr, pixelSize); ptr += pixelSize; } } else fs.read (reinterpret_cast(col.data()), columnSize); columnFromDisk (col, data.data() + static_cast (ix) * ny); } return true; } bool Array2dFile::arrayDataWrite (frnetorderstream& fs) { if (! fs) { sys_error (ERR_WARNING, "Tried to arrayDataWrite with !fs"); return false; } if (m_real.empty()) return false; fs.seekp (m_headersize); if (m_pixelSize == static_cast(sizeof(kfloat64))) writeColumns (fs, m_real, m_nx, m_ny, m_pixelSize); else writeColumns (fs, m_real, m_nx, m_ny, m_pixelSize); if (m_dataType == DATA_TYPE_COMPLEX) { if (m_pixelSize == static_cast(sizeof(kfloat64))) writeColumns (fs, m_imag, m_nx, m_ny, m_pixelSize); else writeColumns (fs, m_imag, m_nx, m_ny, m_pixelSize); } return true; } bool Array2dFile::arrayDataRead (frnetorderstream& fs) { if (! fs) { sys_error (ERR_WARNING, "Tried to arrayDataRead with ! fs"); return false; } if (m_real.empty()) return false; fs.seekg (m_headersize); if (m_pixelSize == static_cast(sizeof(kfloat64))) readColumns (fs, m_real, m_nx, m_ny, m_pixelSize); else readColumns (fs, m_real, m_nx, m_ny, m_pixelSize); if (m_dataType == DATA_TYPE_COMPLEX) { if (m_pixelSize == static_cast(sizeof(kfloat64))) readColumns (fs, m_imag, m_nx, m_ny, m_pixelSize); else readColumns (fs, m_imag, m_nx, m_ny, m_pixelSize); } return true; } bool Array2dFile::labelsRead (frnetorderstream& fs) { std::streamoff pos = m_headersize + m_arraySize; fs.seekg (pos); if (fs.fail()) return false; for (int i = 0; i < m_numFileLabels; i++) { kuint16 labelType, year, month, day, hour, minute, second; kfloat64 calcTime; fs.readInt16 (labelType); fs.readInt16 (year); fs.readInt16 (month); fs.readInt16 (day); fs.readInt16 (hour); fs.readInt16 (minute); fs.readInt16 (second); fs.readFloat64 (calcTime); kuint16 strLength; fs.readInt16 (strLength); std::vector vecLabelStr (strLength + 1); char* pszLabelStr = vecLabelStr.data(); fs.read (pszLabelStr, strLength); pszLabelStr[strLength] = 0; Array2dFileLabel* pLabel = new Array2dFileLabel (labelType, pszLabelStr, calcTime); pLabel->setDateTime (year, month, day, hour, minute, second); m_labels.push_back (pLabel); } return true; } bool Array2dFile::labelsWrite (frnetorderstream& fs) { std::streamoff pos = m_headersize + m_arraySize; fs.seekp (pos); for (constLabelIterator l = m_labels.begin(); l != m_labels.end(); l++) { const Array2dFileLabel& label = **l; kuint16 labelType = label.getLabelType(); kfloat64 calcTime = label.getCalcTime(); const char* const labelString = label.getLabelString().c_str(); int year, month, day, hour, minute, second; kuint16 yearBuf, monthBuf, dayBuf, hourBuf, minuteBuf, secondBuf; label.getDateTime (year, month, day, hour, minute, second); yearBuf = year; monthBuf = month; dayBuf = day; hourBuf = hour; minuteBuf = minute; secondBuf = second; fs.writeInt16 (labelType); fs.writeInt16 (yearBuf); fs.writeInt16 (monthBuf); fs.writeInt16 (dayBuf); fs.writeInt16 (hourBuf); fs.writeInt16 (minuteBuf); fs.writeInt16 (secondBuf); fs.writeFloat64 (calcTime); kuint16 strlength = strlen (labelString); fs.writeInt16 (strlength); fs.write (labelString, strlength); } return true; } void Array2dFile::labelAdd (std::string_view lstr, double calc_time) { labelAdd (Array2dFileLabel::L_HISTORY, lstr, calc_time); } void Array2dFile::labelAdd (int type, std::string_view lstr, double calc_time) { Array2dFileLabel label (type, lstr, calc_time); labelAdd (label); } void Array2dFile::labelAdd (const Array2dFileLabel& label) { Array2dFileLabel* pLabel = new Array2dFileLabel(label); m_labels.push_back (pLabel); } void Array2dFile::labelsCopy (const Array2dFile& copyFile, std::string_view idStr) { std::string id; if (! idStr.empty()) id = idStr; for (unsigned int i = 0; i < copyFile.getNumLabels(); i++) { Array2dFileLabel l (copyFile.labelGet (i)); std::string lstr = l.getLabelString(); lstr = id + lstr; l.setLabelString (lstr); labelAdd (l); } } void Array2dFile::arrayDataClear (void) { std::fill (m_real.begin(), m_real.end(), 0.); std::fill (m_imag.begin(), m_imag.end(), 0.); } void Array2dFile::printLabels (std::ostream& os) const { for (constLabelIterator l = m_labels.begin(); l != m_labels.end(); l++) { const Array2dFileLabel& label = **l; label.print (os); os << std::endl; } } void Array2dFile::printLabelsBrief (std::ostream& os) const { for (constLabelIterator l = m_labels.begin(); l != m_labels.end(); l++) { const Array2dFileLabel& label = **l; label.printBrief (os); } } void Array2dFile::printLabelsBrief (std::ostringstream& os) const { for (constLabelIterator l = m_labels.begin(); l != m_labels.end(); l++) { const Array2dFileLabel& label = **l; label.printBrief (os); } } const Array2dFileLabel& Array2dFile::labelGet (int i) const { return *m_labels[i]; } // --------------------------------------------------------------------------- // Structured history and extensible metadata (6.5.0) // --------------------------------------------------------------------------- HistoryParams& HistoryParams::add (std::string_view key, std::string_view value) { m_vecParams.push_back (std::make_pair (std::string (key), std::string (value))); return *this; } HistoryParams& HistoryParams::add (std::string_view key, double dValue) { std::ostringstream os; os << std::setprecision (12) << dValue; // enough to reproduce a run return add (key, std::string_view (os.str())); } HistoryParams& HistoryParams::add (std::string_view key, int iValue) { std::ostringstream os; os << iValue; return add (key, std::string_view (os.str())); } HistoryParams& HistoryParams::add (std::string_view key, bool bValue) { return add (key, std::string_view (bValue ? "true" : "false")); } std::string HistoryParams::toString() const { std::string str; for (size_t i = 0; i < m_vecParams.size(); i++) { if (i > 0) str += ", "; str += m_vecParams[i].first + "=" + m_vecParams[i].second; } return str; } // One "key=value" per line. Keys cannot contain '=' and values cannot contain // a newline; both are enforced by replacing the offending character, since a // parameter is never important enough to justify an escaping scheme. std::string HistoryParams::encode() const { std::string str; for (size_t i = 0; i < m_vecParams.size(); i++) { std::string strKey = m_vecParams[i].first; std::string strValue = m_vecParams[i].second; std::replace (strKey.begin(), strKey.end(), '=', '_'); std::replace (strKey.begin(), strKey.end(), '\n', ' '); std::replace (strValue.begin(), strValue.end(), '\n', ' '); str += strKey + "=" + strValue + "\n"; } return str; } void HistoryParams::decode (const std::string& strEncoded) { m_vecParams.clear(); std::istringstream is (strEncoded); std::string strLine; while (std::getline (is, strLine)) { if (strLine.empty()) continue; const std::string::size_type iEq = strLine.find ('='); if (iEq == std::string::npos) continue; m_vecParams.push_back (std::make_pair (strLine.substr (0, iEq), strLine.substr (iEq + 1))); } } void Array2dFile::addHistory (std::string_view operation, const HistoryParams& params, double dCalcTime) { Array2dFileLabel label; label.setLabelType (Array2dFileLabel::L_HISTORY); label.setOperation (operation); label.setParameters (params); label.setCalcTime (dCalcTime); #ifdef VERSION label.setVersion (VERSION); #endif std::string strText (operation); if (! params.empty()) strText += ": " + params.toString(); label.setLabelString (strText); labelAdd (label); } void Array2dFile::setUserAttribute (std::string_view name, std::string_view value) { m_mapUser[std::string (name)] = std::string (value); } void Array2dFile::setUserAttribute (std::string_view name, double dValue) { std::ostringstream os; os << std::setprecision (17) << dValue; m_mapUser[std::string (name)] = os.str(); } bool Array2dFile::getUserAttribute (std::string_view name, std::string& strValue) const { const auto it = m_mapUser.find (name); if (it == m_mapUser.end()) return false; strValue = it->second; return true; } double Array2dFile::getUserAttributeDouble (std::string_view name, double dDefault) const { std::string strValue; if (! getUserAttribute (name, strValue)) return dDefault; std::istringstream is (strValue); double dValue = dDefault; is >> dValue; return dValue; } ctsim-7.0.0/libctsim/procsignal.cpp0000644000175000017500000007421015253543770016341 0ustar kevinkevin/***************************************************************************** ** File IDENTIFICATION ** ** Name: procsignal.cpp ** Purpose: Routines for processing signals and projections ** Progammer: Kevin Rosenberg ** Date Started: Aug 1984 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #include "ct.h" #include #include "ctsupport_hooks.h" // FilterMethod ID/Names const int ProcessSignal::FILTER_METHOD_INVALID = -1; const int ProcessSignal::FILTER_METHOD_CONVOLUTION = 0; const int ProcessSignal::FILTER_METHOD_FOURIER = 1; const int ProcessSignal::FILTER_METHOD_FOURIER_TABLE = 2; const int ProcessSignal::FILTER_METHOD_FFT = 3; #if HAVE_FFTW const int ProcessSignal::FILTER_METHOD_FFTW = 4; const int ProcessSignal::FILTER_METHOD_RFFTW =5 ; #endif const char* const ProcessSignal::s_aszFilterMethodName[] = { "convolution", "fourier", "fouier-table", "fft", #if HAVE_FFTW "fftw", "rfftw", #endif }; const char* const ProcessSignal::s_aszFilterMethodTitle[] = { "Convolution", "Fourier", "Fouier Trigometric Table", "FFT", #if HAVE_FFTW "FFTW", "Real/Half-Complex FFTW", #endif }; const int ProcessSignal::s_iFilterMethodCount = sizeof(s_aszFilterMethodName) / sizeof(const char*); // FilterGeneration ID/Names const int ProcessSignal::FILTER_GENERATION_INVALID = -1; const int ProcessSignal::FILTER_GENERATION_DIRECT = 0; const int ProcessSignal::FILTER_GENERATION_INVERSE_FOURIER = 1; const char* const ProcessSignal::s_aszFilterGenerationName[] = { "direct", "inverse-fourier", }; const char* const ProcessSignal::s_aszFilterGenerationTitle[] = { "Direct", "Inverse Fourier", }; const int ProcessSignal::s_iFilterGenerationCount = sizeof(s_aszFilterGenerationName) / sizeof(const char*); // CLASS IDENTIFICATION // ProcessSignal // ProcessSignal::ProcessSignal (std::string_view svFilterName, std::string_view svFilterMethodName, double dBandwidth, double dSignalIncrement, int nSignalPoints, double dFilterParam, std::string_view svDomainName, std::string_view svFilterGenerationName, int iZeropad, int iPreinterpolationFactor, int iTraceLevel, int iGeometry, double dFocalLength, double dSourceDetectorLength, SGP* pSGP) : m_fail (false) { m_idFilterMethod = convertFilterMethodNameToID (svFilterMethodName); if (m_idFilterMethod == FILTER_METHOD_INVALID) { m_fail = true; m_failMessage = "Invalid filter method name "; m_failMessage += svFilterMethodName; return; } m_idFilterGeneration = convertFilterGenerationNameToID (svFilterGenerationName); if (m_idFilterGeneration == FILTER_GENERATION_INVALID) { m_fail = true; m_failMessage = "Invalid frequency filter name "; m_failMessage += svFilterGenerationName; return; } m_idFilter = SignalFilter::convertFilterNameToID (svFilterName); if (m_idFilter == SignalFilter::FILTER_INVALID) { m_fail = true; m_failMessage = "Invalid Filter name "; m_failMessage += svFilterName; return; } m_idDomain = SignalFilter::convertDomainNameToID (svDomainName); if (m_idDomain == SignalFilter::DOMAIN_INVALID) { m_fail = true; m_failMessage = "Invalid domain name "; m_failMessage += svDomainName; return; } init (m_idFilter, m_idFilterMethod, dBandwidth, dSignalIncrement, nSignalPoints, dFilterParam, m_idDomain, m_idFilterGeneration, iZeropad, iPreinterpolationFactor, iTraceLevel, iGeometry, dFocalLength, dSourceDetectorLength, pSGP); } void ProcessSignal::init (const int idFilter, const int idFilterMethod, double dBandwidth, double dSignalIncrement, int nSignalPoints, double dFilterParam, const int idDomain, const int idFilterGeneration, const int iZeropad, const int iPreinterpolationFactor, int iTraceLevel, int iGeometry, double dFocalLength, double dSourceDetectorLength, SGP* pSGP) { int i; m_idFilter = idFilter; m_idDomain = idDomain; m_idFilterMethod = idFilterMethod; m_idFilterGeneration = idFilterGeneration; m_idGeometry = iGeometry; m_dFocalLength = dFocalLength; m_dSourceDetectorLength = dSourceDetectorLength; if (m_idFilter == SignalFilter::FILTER_INVALID || m_idDomain == SignalFilter::DOMAIN_INVALID || m_idFilterMethod == FILTER_METHOD_INVALID || m_idFilterGeneration == FILTER_GENERATION_INVALID) { m_fail = true; return; } m_traceLevel = iTraceLevel; m_nameFilterMethod = convertFilterMethodIDToName (m_idFilterMethod); m_nameFilterGeneration = convertFilterGenerationIDToName (m_idFilterGeneration); m_dBandwidth = dBandwidth; m_nSignalPoints = nSignalPoints; m_dSignalInc = dSignalIncrement; m_dFilterParam = dFilterParam; m_iZeropad = iZeropad; m_iPreinterpolationFactor = iPreinterpolationFactor; // scale signalInc/BW to adjust for imaginary detector through origin of phantom // see Kak-Slaney Fig 3.22, for Collinear diagram if (m_idGeometry == Scanner::GEOMETRY_EQUILINEAR) { double dEquilinearScale = m_dSourceDetectorLength / m_dFocalLength; m_dSignalInc /= dEquilinearScale; m_dBandwidth *= dEquilinearScale; } if (m_idFilterMethod == FILTER_METHOD_FFT) { #if HAVE_FFTW m_idFilterMethod = FILTER_METHOD_RFFTW; #else m_fail = true; m_failMessage = "FFT not yet implemented"; return; #endif } bool m_bFrequencyFiltering = true; if (m_idFilterMethod == FILTER_METHOD_CONVOLUTION) m_bFrequencyFiltering = false; // Spatial-based filtering if (! m_bFrequencyFiltering) { if (m_idFilterGeneration == FILTER_GENERATION_DIRECT) { m_nFilterPoints = 2 * (m_nSignalPoints - 1) + 1; m_dFilterMin = -m_dSignalInc * (m_nSignalPoints - 1); m_dFilterMax = m_dSignalInc * (m_nSignalPoints - 1); m_dFilterInc = (m_dFilterMax - m_dFilterMin) / (m_nFilterPoints - 1); SignalFilter filter (m_idFilter, m_dFilterMin, m_dFilterMax, m_nFilterPoints, m_dBandwidth, m_dFilterParam, SignalFilter::DOMAIN_SPATIAL); m_vecFilter.assign (m_nFilterPoints, 0.); filter.copyFilterData (m_vecFilter.data(), 0, m_nFilterPoints); } else if (m_idFilterGeneration == FILTER_GENERATION_INVERSE_FOURIER) { m_nFilterPoints = 2 * (m_nSignalPoints - 1) + 1; m_dFilterMin = -1. / (2 * m_dSignalInc); m_dFilterMax = 1. / (2 * m_dSignalInc); m_dFilterInc = (m_dFilterMax - m_dFilterMin) / (m_nFilterPoints - 1); SignalFilter filter (m_idFilter, m_dFilterMin, m_dFilterMax, m_nFilterPoints, m_dBandwidth, m_dFilterParam, SignalFilter::DOMAIN_FREQUENCY); m_vecFilter.assign (m_nFilterPoints, 0.); std::vector adFrequencyFilter (m_nFilterPoints); filter.copyFilterData (adFrequencyFilter.data(), 0, m_nFilterPoints); if (m_traceLevel > 0) tracePlot ("Filter Response: Natural Order", {}, std::span (adFrequencyFilter)); Fourier::shuffleNaturalToFourierOrder (adFrequencyFilter.data(), m_nFilterPoints); if (m_traceLevel > 0) tracePlot ("Filter Response: Fourier Order", {}, std::span (adFrequencyFilter)); ProcessSignal::finiteFourierTransform (adFrequencyFilter.data(), m_vecFilter.data(), m_nFilterPoints, FORWARD); if (m_traceLevel > 0) tracePlot ("Inverse Fourier Frequency: Fourier Order", {}, std::span (m_vecFilter.data(), m_nFilterPoints)); Fourier::shuffleFourierToNaturalOrder (m_vecFilter.data(), m_nFilterPoints); if (m_traceLevel > 0) tracePlot ("Inverse Fourier Frequency: Natural Order", {}, std::span (m_vecFilter.data(), m_nFilterPoints)); for (i = 0; i < m_nFilterPoints; i++) { m_vecFilter[i] /= m_dSignalInc; } } if (m_idGeometry == Scanner::GEOMETRY_EQUILINEAR) { for (i = 0; i < m_nFilterPoints; i++) m_vecFilter[i] *= 0.5; } else if (m_idGeometry == Scanner::GEOMETRY_EQUIANGULAR) { for (i = 0; i < m_nFilterPoints; i++) { int iDetFromZero = i - ((m_nFilterPoints - 1) / 2); double sinScale = 1 / SignalFilter::sinc (iDetFromZero * m_dSignalInc); double dScale = 0.5 * sinScale * sinScale; m_vecFilter[i] *= dScale; } if (m_traceLevel > 0) tracePlot ("Scaled Inverse Fourier Frequency: Natural Order", {}, std::span (m_vecFilter.data(), m_nFilterPoints)); } // if (geometry) } // if (spatial filtering) else if (m_bFrequencyFiltering) { // Frequency-based filtering if (m_idFilterGeneration == FILTER_GENERATION_DIRECT) { // calculate number of filter points with zeropadding m_nFilterPoints = addZeropadFactor (m_nSignalPoints, m_iZeropad); m_nOutputPoints = m_nFilterPoints * m_iPreinterpolationFactor; if (isOdd (m_nFilterPoints)) { // Odd m_dFilterMin = -1. / (2 * m_dSignalInc); m_dFilterMax = 1. / (2 * m_dSignalInc); m_dFilterInc = (m_dFilterMax - m_dFilterMin) / (m_nFilterPoints - 1); } else { // Even m_dFilterMin = -1. / (2 * m_dSignalInc); m_dFilterMax = 1. / (2 * m_dSignalInc); m_dFilterInc = (m_dFilterMax - m_dFilterMin) / m_nFilterPoints; m_dFilterMax -= m_dFilterInc; } SignalFilter filter (m_idFilter, m_dFilterMin, m_dFilterMax, m_nFilterPoints, m_dBandwidth, m_dFilterParam, SignalFilter::DOMAIN_FREQUENCY); m_vecFilter.assign (m_nFilterPoints, 0.); filter.copyFilterData (m_vecFilter.data(), 0, m_nFilterPoints); if (m_traceLevel > 0) tracePlot ("Frequency Filter: Natural Order", {}, std::span (m_vecFilter.data(), m_nFilterPoints)); // This works fairly well. I'm not sure why since scaling for geometries is done on // frequency filter rather than spatial filter as it should be. // It gives values slightly off than freq/inverse filtering if (m_idGeometry == Scanner::GEOMETRY_EQUILINEAR) { for (i = 0; i < m_nFilterPoints; i++) m_vecFilter[i] *= 0.5; } else if (m_idGeometry == Scanner::GEOMETRY_EQUIANGULAR) { for (i = 0; i < m_nFilterPoints; i++) { int iDetFromZero = i - ((m_nFilterPoints - 1) / 2); double sinScale = 1 / SignalFilter::sinc (iDetFromZero * m_dSignalInc); double dScale = 0.5 * sinScale * sinScale; m_vecFilter[i] *= dScale; } } if (m_traceLevel > 0) tracePlot ("Filter Geometry Scaled: Natural Order", {}, std::span (m_vecFilter.data(), m_nFilterPoints)); Fourier::shuffleNaturalToFourierOrder (m_vecFilter.data(), m_nFilterPoints); if (m_traceLevel > 0) tracePlot ("Filter Geometry Scaled: Fourier Order", {}, std::span (m_vecFilter.data(), m_nFilterPoints)); // FILTERING: FREQUENCY - INVERSE FOURIER } else if (m_idFilterGeneration == FILTER_GENERATION_INVERSE_FOURIER) { // calculate number of filter points with zeropadding int nSpatialPoints = 2 * (m_nSignalPoints - 1) + 1; m_dFilterMin = -m_dSignalInc * (m_nSignalPoints - 1); m_dFilterMax = m_dSignalInc * (m_nSignalPoints - 1); m_dFilterInc = (m_dFilterMax - m_dFilterMin) / (nSpatialPoints - 1); m_nFilterPoints = nSpatialPoints; if (m_iZeropad > 0) { double logBase2 = log(nSpatialPoints) / log(2); int nextPowerOf2 = static_cast(floor(logBase2)); if (logBase2 != floor(logBase2)) nextPowerOf2++; nextPowerOf2 += (m_iZeropad - 1); m_nFilterPoints = 1 << nextPowerOf2; } m_nOutputPoints = m_nFilterPoints * m_iPreinterpolationFactor; #if defined(DEBUG) || defined(_DEBUG) if (m_traceLevel >= Trace::TRACE_CONSOLE) sys_error (ERR_TRACE, "nFilterPoints = {}", m_nFilterPoints); #endif std::vector adSpatialFilter (m_nFilterPoints); SignalFilter filter (m_idFilter, m_dFilterMin, m_dFilterMax, nSpatialPoints, m_dBandwidth, m_dFilterParam, SignalFilter::DOMAIN_SPATIAL); filter.copyFilterData (adSpatialFilter.data(), 0, nSpatialPoints); if (m_traceLevel > 0) tracePlot ("Spatial Filter: Natural Order", {}, std::span (adSpatialFilter.data(), nSpatialPoints)); if (m_idGeometry == Scanner::GEOMETRY_EQUILINEAR) { for (i = 0; i < nSpatialPoints; i++) adSpatialFilter[i] *= 0.5; } else if (m_idGeometry == Scanner::GEOMETRY_EQUIANGULAR) { for (i = 0; i < nSpatialPoints; i++) { int iDetFromZero = i - ((nSpatialPoints - 1) / 2); double sinScale = sin (iDetFromZero * m_dSignalInc); if (fabs(sinScale) < 1E-7) sinScale = 1; else sinScale = (iDetFromZero * m_dSignalInc) / sinScale; double dScale = 0.5 * sinScale * sinScale; adSpatialFilter[i] *= dScale; } } if (m_traceLevel > 0) tracePlot ("Scaled Spatial Filter: Natural Order", {}, std::span (adSpatialFilter.data(), nSpatialPoints)); for (i = nSpatialPoints; i < m_nFilterPoints; i++) adSpatialFilter[i] = 0; m_vecFilter.assign (m_nFilterPoints, 0.); std::vector> acInverseFilter (m_nFilterPoints); finiteFourierTransform (adSpatialFilter.data(), acInverseFilter.data(), m_nFilterPoints, BACKWARD); for (i = 0; i < m_nFilterPoints; i++) m_vecFilter[i] = std::abs (acInverseFilter[i]) * m_dSignalInc; if (m_traceLevel > 0) tracePlot ("Fourier Scaled Spatial Filter: Fourier Order", {}, std::span (m_vecFilter.data(), m_nFilterPoints)); } } // precalculate sin and cosine tables for fourier transform if (m_idFilterMethod == FILTER_METHOD_FOURIER_TABLE) { int nFourier = imax (m_nFilterPoints,m_nOutputPoints) * imax (m_nFilterPoints, m_nOutputPoints) + 1; double angleIncrement = (2. * PI) / m_nFilterPoints; m_vecFourierCosTable.assign (nFourier, 0.); m_vecFourierSinTable.assign (nFourier, 0.); double angle = 0; for (i = 0; i < nFourier; i++) { m_vecFourierCosTable.data()[i] = cos (angle); m_vecFourierSinTable.data()[i] = sin (angle); angle += angleIncrement; } } #if HAVE_FFTW if (m_idFilterMethod == FILTER_METHOD_FFTW || m_idFilterMethod == FILTER_METHOD_RFFTW) { for (i = 0; i < m_nFilterPoints; i++) //fftw uses unnormalized fft m_vecFilter[i] /= m_nFilterPoints; } if (m_idFilterMethod == FILTER_METHOD_RFFTW) { m_adRealFftInput = static_cast(fftw_malloc (sizeof(double) * m_nFilterPoints)); m_adRealFftOutput = static_cast(fftw_malloc (sizeof(double) * m_nFilterPoints)); m_realPlanForward = fftw_plan_r2r_1d (m_nFilterPoints, m_adRealFftInput, m_adRealFftOutput, FFTW_R2HC, FFTW_ESTIMATE); m_adRealFftSignal = static_cast(fftw_malloc (sizeof(double) * m_nOutputPoints)); m_adRealFftBackwardOutput = static_cast(fftw_malloc (sizeof(double) * m_nOutputPoints)); m_realPlanBackward = fftw_plan_r2r_1d (m_nOutputPoints, m_adRealFftSignal, m_adRealFftBackwardOutput, FFTW_HC2R, FFTW_ESTIMATE); for (i = 0; i < m_nFilterPoints; i++) m_adRealFftInput[i] = 0; } else if (m_idFilterMethod == FILTER_METHOD_FFTW) { m_adComplexFftInput = static_cast(fftw_malloc (sizeof(fftw_complex) * m_nFilterPoints)); m_adComplexFftOutput = static_cast(fftw_malloc (sizeof(fftw_complex) * m_nFilterPoints)); m_complexPlanForward = fftw_plan_dft_1d (m_nFilterPoints, m_adComplexFftInput, m_adComplexFftOutput, FFTW_FORWARD, FFTW_ESTIMATE); m_adComplexFftSignal = static_cast(fftw_malloc (sizeof(fftw_complex) * m_nOutputPoints)); m_adComplexFftBackwardOutput = static_cast(fftw_malloc (sizeof(fftw_complex) * m_nOutputPoints)); m_complexPlanBackward = fftw_plan_dft_1d (m_nOutputPoints, m_adComplexFftSignal, m_adComplexFftBackwardOutput, FFTW_BACKWARD, FFTW_ESTIMATE); for (i = 0; i < m_nFilterPoints; i++) m_adComplexFftInput[i][0] = m_adComplexFftInput[i][1] = 0; for (i = 0; i < m_nOutputPoints; i++) m_adComplexFftSignal[i][0] = m_adComplexFftSignal[i][1] = 0; } #endif } ProcessSignal::~ProcessSignal (void) { #if HAVE_FFTW if (m_idFilterMethod == FILTER_METHOD_FFTW) { fftw_destroy_plan(m_complexPlanForward); fftw_destroy_plan(m_complexPlanBackward); fftw_free (m_adComplexFftInput); fftw_free (m_adComplexFftOutput); fftw_free (m_adComplexFftSignal); fftw_free (m_adComplexFftBackwardOutput); } if (m_idFilterMethod == FILTER_METHOD_RFFTW) { fftw_destroy_plan(m_realPlanForward); fftw_destroy_plan(m_realPlanBackward); fftw_free (m_adRealFftInput); fftw_free (m_adRealFftOutput); fftw_free (m_adRealFftSignal); fftw_free (m_adRealFftBackwardOutput); } #endif } int ProcessSignal::convertFilterMethodNameToID (std::string_view name) { int fmID = FILTER_METHOD_INVALID; for (int i = 0; i < s_iFilterMethodCount; i++) { if (equalsIgnoringCase (name, s_aszFilterMethodName[i])) { fmID = i; break; } } return (fmID); } std::string_view ProcessSignal::convertFilterMethodIDToName (const int fmID) { static const char *name = ""; if (fmID >= 0 && fmID < s_iFilterMethodCount) return (s_aszFilterMethodName [fmID]); return (name); } const char * ProcessSignal::convertFilterMethodIDToTitle (const int fmID) { static const char *title = ""; if (fmID >= 0 && fmID < s_iFilterMethodCount) return (s_aszFilterMethodTitle [fmID]); return (title); } int ProcessSignal::convertFilterGenerationNameToID (std::string_view name) { int fgID = FILTER_GENERATION_INVALID; for (int i = 0; i < s_iFilterGenerationCount; i++) { if (equalsIgnoringCase (name, s_aszFilterGenerationName[i])) { fgID = i; break; } } return (fgID); } std::string_view ProcessSignal::convertFilterGenerationIDToName (const int fgID) { static const char *name = ""; if (fgID >= 0 && fgID < s_iFilterGenerationCount) return (s_aszFilterGenerationName [fgID]); return (name); } const char * ProcessSignal::convertFilterGenerationIDToTitle (const int fgID) { static const char *name = ""; if (fgID >= 0 && fgID < s_iFilterGenerationCount) return (s_aszFilterGenerationTitle [fgID]); return (name); } void ProcessSignal::filterSignal (std::span constInput, std::span output) const { if (static_cast (constInput.size()) < m_nSignalPoints || static_cast (output.size()) < m_nSignalPoints) { sys_error (ERR_SEVERE, "filterSignal given {} in and {} out, {} needed " "[ProcessSignal::filterSignal]", constInput.size(), output.size(), m_nSignalPoints); return; } std::vector input (m_nSignalPoints); int i; #if HAVE_OPENMP #pragma omp parallel for #endif for (i = 0; i < m_nSignalPoints; i++) input[i] = constInput[i]; if (m_idGeometry == Scanner::GEOMETRY_EQUILINEAR) { #if HAVE_OPENMP #pragma omp parallel for #endif for (int i = 0; i < m_nSignalPoints; i++) { int iDetFromCenter = i - (m_nSignalPoints / 2); input[i] *= m_dFocalLength / sqrt (m_dFocalLength * m_dFocalLength + iDetFromCenter * iDetFromCenter * m_dSignalInc * m_dSignalInc); } } else if (m_idGeometry == Scanner::GEOMETRY_EQUIANGULAR) { #if HAVE_OPENMP #pragma omp parallel for #endif for (int i = 0; i < m_nSignalPoints; i++) { int iDetFromCenter = i - (m_nSignalPoints / 2); input[i] *= m_dFocalLength * cos (iDetFromCenter * m_dSignalInc); } } if (m_idFilterMethod == FILTER_METHOD_CONVOLUTION) { #if HAVE_OPENMP #pragma omp parallel for #endif for (i = 0; i < m_nSignalPoints; i++) output[i] = convolve (input.data(), m_dSignalInc, i, m_nSignalPoints); } else if (m_idFilterMethod == FILTER_METHOD_FOURIER) { std::vector inputSignal (m_nFilterPoints); for (i = 0; i < m_nSignalPoints; i++) inputSignal[i] = input[i]; for (i = m_nSignalPoints; i < m_nFilterPoints; i++) inputSignal[i] = 0; // zeropad std::vector> fftSignal (m_nFilterPoints); finiteFourierTransform (inputSignal.data(), fftSignal.data(), m_nFilterPoints, FORWARD); #if HAVE_OPENMP #pragma omp parallel for #endif for (i = 0; i < m_nFilterPoints; i++) fftSignal[i] *= m_vecFilter[i]; std::vector inverseFourier (m_nFilterPoints); finiteFourierTransform (fftSignal.data(), inverseFourier.data(), m_nFilterPoints, BACKWARD); for (i = 0; i < m_nSignalPoints; i++) output[i] = inverseFourier[i]; } else if (m_idFilterMethod == FILTER_METHOD_FOURIER_TABLE) { std::vector inputSignal (m_nFilterPoints); for (i = 0; i < m_nSignalPoints; i++) inputSignal[i] = input[i]; for (i = m_nSignalPoints; i < m_nFilterPoints; i++) inputSignal[i] = 0; // zeropad std::vector> fftSignal (m_nFilterPoints); finiteFourierTransform (inputSignal.data(), fftSignal.data(), FORWARD); #if HAVE_OPENMP #pragma omp parallel for #endif for (i = 0; i < m_nFilterPoints; i++) fftSignal[i] *= m_vecFilter[i]; std::vector inverseFourier (m_nFilterPoints); finiteFourierTransform (fftSignal.data(), inverseFourier.data(), BACKWARD); for (i = 0; i < m_nSignalPoints; i++) output[i] = inverseFourier[i]; } #if HAVE_FFTW else if (m_idFilterMethod == FILTER_METHOD_RFFTW) { for (i = 0; i < m_nSignalPoints; i++) m_adRealFftInput[i] = input[i]; fftw_execute (m_realPlanForward); for (i = 0; i < m_nFilterPoints; i++) m_adRealFftSignal[i] = m_vecFilter[i] * m_adRealFftOutput[i]; for (i = m_nFilterPoints; i < m_nOutputPoints; i++) m_adRealFftSignal[i] = 0; fftw_execute (m_realPlanBackward); for (i = 0; i < m_nSignalPoints * m_iPreinterpolationFactor; i++) output[i] = m_adRealFftBackwardOutput[i]; } else if (m_idFilterMethod == FILTER_METHOD_FFTW) { for (i = 0; i < m_nSignalPoints; i++) m_adComplexFftInput[i][0] = input[i]; fftw_execute (m_complexPlanForward); #if HAVE_OPENMP #pragma omp parallel for #endif for (i = 0; i < m_nFilterPoints; i++) { m_adComplexFftSignal[i][0] = m_vecFilter[i] * m_adComplexFftOutput[i][0]; m_adComplexFftSignal[i][1] = m_vecFilter[i] * m_adComplexFftOutput[i][1]; } fftw_execute (m_complexPlanBackward); for (i = 0; i < m_nSignalPoints * m_iPreinterpolationFactor; i++) output[i] = m_adComplexFftBackwardOutput[i][0]; } #endif } /* NAME * convolve Discrete convolution of two functions * * SYNOPSIS * r = convolve (f1, f2, dx, n, np, func_type) * double r Convolved result * double f1[], f2[] Functions to be convolved * double dx Difference between successive x values * int n Array index to center convolution about * int np Number of points in f1 array * int func_type EVEN or ODD or EVEN_AND_ODD function f2 * * NOTES * f1 is the projection data, its indices range from 0 to np - 1. * The index for f2, the filter, ranges from -(np-1) to (np-1). * There are 3 ways to handle the negative vertices of f2: * 1. If we know f2 is an EVEN function, then f2[-n] = f2[n]. * All filters used in reconstruction are even. * 2. If we know f2 is an ODD function, then f2[-n] = -f2[n] * 3. If f2 is both ODD AND EVEN, then we must store the value of f2 * for negative indices. Since f2 must range from -(np-1) to (np-1), * if we add (np - 1) to f2's array index, then f2's index will * range from 0 to 2 * (np - 1), and the origin, x = 0, will be * stored at f2[np-1]. */ double ProcessSignal::convolve (const double func[], const double dx, const int n, const int np) const { double sum = 0.0; #if UNOPTIMIZED_CONVOLUTION for (int i = 0; i < np; i++) sum += func[i] * m_vecFilter[n - i + (np - 1)]; #else const double* f2 = m_vecFilter.data() + n + (np - 1); for (int i = 0; i < np; i++) sum += *func++ * *f2--; #endif return (sum * dx); } double ProcessSignal::convolve (const float func[], const double dx, const int n, const int np) const { double sum = 0.0; #if UNOPTIMIZED_CONVOLUTION for (int i = 0; i < np; i++) sum += func[i] * m_vecFilter[n - i + (np - 1)]; #else const double* f2 = m_vecFilter.data() + n + (np - 1); for (int i = 0; i < np; i++) sum += *func++ * *f2--; #endif return (sum * dx); } void ProcessSignal::finiteFourierTransform (const double input[], double output[], const int n, int direction) { std::vector> complexOutput (n); finiteFourierTransform (input, complexOutput.data(), n, direction); for (int i = 0; i < n; i++) output[i] = complexOutput[i].real(); } void ProcessSignal::finiteFourierTransform (const double input[], std::complex output[], const int n, int direction) { if (direction < 0) direction = -1; else direction = 1; double angleIncrement = direction * 2 * PI / n; for (int i = 0; i < n; i++) { double sumReal = 0; double sumImag = 0; for (int j = 0; j < n; j++) { double angle = i * j * angleIncrement; sumReal += input[j] * cos(angle); sumImag += input[j] * sin(angle); } if (direction < 0) { sumReal /= n; sumImag /= n; } output[i] = std::complex (sumReal, sumImag); } } void ProcessSignal::finiteFourierTransform (const std::complex input[], std::complex output[], const int n, int direction) { if (direction < 0) direction = -1; else direction = 1; double angleIncrement = direction * 2 * PI / n; for (int i = 0; i < n; i++) { std::complex sum (0,0); for (int j = 0; j < n; j++) { double angle = i * j * angleIncrement; std::complex exponentTerm (cos(angle), sin(angle)); sum += input[j] * exponentTerm; } if (direction < 0) { sum /= n; } output[i] = sum; } } void ProcessSignal::finiteFourierTransform (const std::complex input[], double output[], const int n, int direction) { if (direction < 0) direction = -1; else direction = 1; double angleIncrement = direction * 2 * PI / n; for (int i = 0; i < n; i++) { double sumReal = 0; for (int j = 0; j < n; j++) { double angle = i * j * angleIncrement; sumReal += input[j].real() * cos(angle) - input[j].imag() * sin(angle); } if (direction < 0) { sumReal /= n; } output[i] = sumReal; } } // Table-based routines void ProcessSignal::finiteFourierTransform (const double input[], std::complex output[], int direction) const { if (direction < 0) direction = -1; else direction = 1; for (int i = 0; i < m_nFilterPoints; i++) { double sumReal = 0, sumImag = 0; for (int j = 0; j < m_nFilterPoints; j++) { int tableIndex = i * j; if (direction > 0) { sumReal += input[j] * m_vecFourierCosTable.data()[tableIndex]; sumImag += input[j] * m_vecFourierSinTable.data()[tableIndex]; } else { sumReal += input[j] * m_vecFourierCosTable.data()[tableIndex]; sumImag -= input[j] * m_vecFourierSinTable.data()[tableIndex]; } } if (direction < 0) { sumReal /= m_nFilterPoints; sumImag /= m_nFilterPoints; } output[i] = std::complex (sumReal, sumImag); } } // (a+bi) * (c + di) = (ac - bd) + (ad + bc)i void ProcessSignal::finiteFourierTransform (const std::complex input[], std::complex output[], int direction) const { if (direction < 0) direction = -1; else direction = 1; for (int i = 0; i < m_nFilterPoints; i++) { double sumReal = 0, sumImag = 0; for (int j = 0; j < m_nFilterPoints; j++) { int tableIndex = i * j; if (direction > 0) { sumReal += input[j].real() * m_vecFourierCosTable.data()[tableIndex] - input[j].imag() * m_vecFourierSinTable.data()[tableIndex]; sumImag += input[j].real() * m_vecFourierSinTable.data()[tableIndex] + input[j].imag() * m_vecFourierCosTable.data()[tableIndex]; } else { sumReal += input[j].real() * m_vecFourierCosTable.data()[tableIndex] - input[j].imag() * -m_vecFourierSinTable.data()[tableIndex]; sumImag += input[j].real() * -m_vecFourierSinTable.data()[tableIndex] + input[j].imag() * m_vecFourierCosTable.data()[tableIndex]; } } if (direction < 0) { sumReal /= m_nFilterPoints; sumImag /= m_nFilterPoints; } output[i] = std::complex (sumReal, sumImag); } } void ProcessSignal::finiteFourierTransform (const std::complex input[], double output[], int direction) const { if (direction < 0) direction = -1; else direction = 1; for (int i = 0; i < m_nFilterPoints; i++) { double sumReal = 0; for (int j = 0; j < m_nFilterPoints; j++) { int tableIndex = i * j; if (direction > 0) { sumReal += input[j].real() * m_vecFourierCosTable.data()[tableIndex] - input[j].imag() * m_vecFourierSinTable.data()[tableIndex]; } else { sumReal += input[j].real() * m_vecFourierCosTable.data()[tableIndex] - input[j].imag() * -m_vecFourierSinTable.data()[tableIndex]; } } if (direction < 0) { sumReal /= m_nFilterPoints; } output[i] = sumReal; } } int ProcessSignal::addZeropadFactor (int n, int iZeropad) { if (iZeropad > 0) { double dLogBase2 = log(n) / log(2); int iLogBase2 = static_cast(floor (dLogBase2)); int iPaddedN = 1 << (iLogBase2 + iZeropad); #ifdef DEBUG sys_error (ERR_TRACE, "Zeropadding {} to {}", n, iPaddedN); #endif return iPaddedN; } return n; } ctsim-7.0.0/libctsim/projections.cpp0000644000175000017500000017662315253765647016563 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: projections.cpp Projection data classes ** Programmer: Kevin Rosenberg ** Date Started: Aug 84 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #include "ct.h" #include "timer.h" #include #include #include #include #include #include "fileformat.h" #include #include "interpolator.h" const kuint16 Projections::m_signature = ('P'*256 + 'J'); const int Projections::POLAR_INTERP_INVALID = -1; const int Projections::POLAR_INTERP_NEAREST = 0; const int Projections::POLAR_INTERP_BILINEAR = 1; const int Projections::POLAR_INTERP_BICUBIC = 2; const char* const Projections::s_aszInterpName[] = { "nearest", "bilinear", // {"bicubic"}, }; const char* const Projections::s_aszInterpTitle[] = { "Nearest", "Bilinear", // {"Bicubic"}, }; const int Projections::s_iInterpCount = sizeof(s_aszInterpName) / sizeof(char*); /* NAME * Projections Constructor for projections matrix storage * * SYNOPSIS * proj = projections_create (filename, nView, nDet) * Projections& proj Allocated projections structure & matrix * int nView Number of rotated view * int nDet Number of detectors * */ Projections::Projections (const Scanner& scanner) : m_projData(0) { initFromScanner (scanner); } Projections::Projections (const int nView, const int nDet) : m_projData(0) { init (nView, nDet); } Projections::Projections (void) : m_projData(0) { init (0, 0); } Projections::~Projections (void) { deleteProjData(); } int Projections::convertInterpNameToID (std::string_view name) { int interpID = POLAR_INTERP_INVALID; for (int i = 0; i < s_iInterpCount; i++) { if (equalsIgnoringCase (name, s_aszInterpName[i])) { interpID = i; break; } } return (interpID); } const char* Projections::convertInterpIDToName (const int interpID) { static const char *interpName = ""; if (interpID >= 0 && interpID < s_iInterpCount) { return (s_aszInterpName[interpID]); } return (interpName); } const char* Projections::convertInterpIDToTitle (const int interpID) { static const char *interpTitle = ""; if (interpID >= 0 && interpID < s_iInterpCount) { return (s_aszInterpTitle[interpID]); } return (interpTitle); } void Projections::init (const int nView, const int nDet) { m_label.setLabelType (Array2dFileLabel::L_HISTORY); m_nView = nView; m_nDet = nDet; newProjData (); time_t t = time (nullptr); tm* lt = localtime (&t); m_year = lt->tm_year; m_month = lt->tm_mon; m_day = lt->tm_mday; m_hour = lt->tm_hour; m_minute = lt->tm_min; m_second = lt->tm_sec; } void Projections::initFromScanner (const Scanner& scanner) { m_label.setLabelType (Array2dFileLabel::L_HISTORY); deleteProjData(); init (scanner.nView(), scanner.nDet()); m_rotInc = scanner.rotInc(); m_detInc = scanner.detInc(); m_detStart = scanner.detStart(); m_geometry = scanner.geometry(); m_dFocalLength = scanner.focalLength(); m_dSourceDetectorLength = scanner.sourceDetectorLength(); m_dViewDiameter = scanner.viewDiameter(); m_rotStart = scanner.offsetView()*scanner.rotInc(); m_dFanBeamAngle = scanner.fanBeamAngle(); } void Projections::setNView (int nView) // used by MPI to reduce # of views { deleteProjData(); init (nView, m_nDet); } // Helical 180 Linear Interpolation. // This member function takes a set of helical scan projections and // performs a linear interpolation between pairs of complementary rays // to produce a single projection data set approximating what would be // measured at a single axial plane. // Complementary rays are rays which traverse the same path through the // phantom in opposite directions. // // For parallel beam geometry, a ray with a given gantry angle beta and a // detector iDet will have a complementary ray at beta + pi and nDet-iDet // // For equiangular or equilinear beam geometry the complementary ray to // gantry angle beta and fan-beam angle gamma is at // beta-hat = beta +2*gamma + pi, and gamma-hat = -gamma. // Note that beta-hat - beta depends on gamma and is not constant. // // The algorithm used here is from Crawford and King, Med. Phys. 17(6) // 1990 p967; what they called method "C", CSH-HH. It uses interpolation only // between pairs of complementary rays on either side of an image plane. // Input data must sample gantry angles from zero to // (2*pi + 2* fan-beam-angle). The data set produced contains gantry // angles from 0 to Pi+fan-beam-angle. This is a "halfscan" data set, // which still contains redundant data, and can be used with a half scan // reconstruction to produce an image. // In this particular implementation a lower triangle from (beta,gamma) = // (0,-fanAngle/2)->(2*fanAngle,-fanAngle/2)->(0,fanAngle/2) contains // zeros, but is actually redundant with data contained in the region // (pi+fanAngle,-fanAngle/2)->(pi+fanAngle, fanAngle/2) ->(pi-fanAngle, // fanAngle/2). // int Projections::Helical180LI(int interpolation_view) { if (m_geometry == Scanner::GEOMETRY_INVALID) { std::cerr << "Invalid geometry " << m_geometry << std::endl; return (2); } else if (m_geometry == Scanner::GEOMETRY_PARALLEL) { std::cerr << "Helical 180LI not yet implemented for PARALLEL geometry" << std::endl; return (2); } else if (m_geometry == Scanner::GEOMETRY_EQUILINEAR) { std::cerr << "Helical 180LI not yet implemented for EQUILINEAR geometry" << std::endl; return (2); } else if (m_geometry == Scanner::GEOMETRY_EQUIANGULAR) { return Helical180LI_Equiangular(interpolation_view); } else { std::cerr << "Invalid geometry in projection data file" << m_geometry << std::endl; return (2); } } int Projections::Helical180LI_Equiangular(int interpView) { double dbeta = m_rotInc; double dgamma = m_detInc; double fanAngle = m_dFanBeamAngle; int offsetView=0; // is there enough data in the data set? Should have 2(Pi+fanAngle) // coverage minimum if ( m_nView < static_cast((2*( PI + fanAngle ) ) / dbeta) -1 ){ std::cerr << "Data set does not include 360 +2*FanBeamAngle views" << std::endl; return (1); } if (interpView < 0) // use default position at PI+fanAngle { interpView = static_cast ((PI+fanAngle)/dbeta); } else { // check if there is PI+fanAngle data on either side of the // of the specified image plane if ( interpView*dbeta < PI+fanAngle || interpView*dbeta + PI + fanAngle > m_nView*dbeta) { std::cerr << "There isn't PI+fanAngle of data on either side of the requested interpolation view" << std::endl; return(1); } offsetView = interpView - static_cast((PI+fanAngle)/dbeta); } int last_interp_view = static_cast ((PI+fanAngle)/dbeta); // make a new array for data... class DetectorArray ** newdetarray = new DetectorArray * [last_interp_view+1]; for ( int i=0 ; i <= last_interp_view ; i++ ){ newdetarray[i] = new DetectorArray (m_nDet); newdetarray[i]->setViewAngle((i+offsetView)*dbeta); const std::span newdetval = (newdetarray[i])->detValues(); // and initialize the data to zero for (int j=0; j < m_nDet; j++) newdetval[j] = 0.; } int last_acq_view = 2*last_interp_view; for ( int iView = 0 ; iView <= last_acq_view; iView++) { double beta = iView * dbeta; for ( int iDet = 0; iDet < m_nDet; iDet++) { double gamma = (iDet -(m_nDet-1)/2)* dgamma ; int newiView, newiDet; if (beta < PI+fanAngle) { //if (PI +fanAngle - beta > dbeta ) //newbeta = beta; //newgamma = gamma; newiDet = iDet; newiView = iView; } else // (beta > PI+fanAngle) { //newbeta = beta +2*gamma - 180; //newgamma = -gamma; newiDet = -iDet + (m_nDet -1); // newiView = nearest((beta + 2*gamma - PI)/dbeta); //newiView = static_cast(( (iView*dbeta) + 2*(iDet-(m_nDet-1)/2)*dgamma - PI)/dbeta); newiView = nearest(( (iView*dbeta) + 2*(iDet-(m_nDet-1)/2)*dgamma - PI)/dbeta); } #ifdef DEBUG //std::cout << beta << " "<< gamma << " " << newbeta << " " << newgamma <<" " << iView-offsetView << " " << iDet << " " << newiView << " " << newiDet << std::endl; //std::cout << iView-offsetView << " " << iDet << " " << newiView << " " << newiDet << std::endl; #endif // last_interp_view truncates (PI+fanAngle)/dbeta while newiView // rounds, so the two can differ by one. A sample outside the // interpolated range has nowhere to go and is dropped; it is // the one at the very edge of the half turn. if (newiView < 0 || newiView > last_interp_view) continue; if ( ( beta > fanAngle - 2*gamma) && ( beta < 2*PI + fanAngle -2*gamma) ) { // not in region 1 or 8 const std::span detval = (m_projData[iView+offsetView])->detValues(); const std::span newdetval = (newdetarray[newiView])->detValues(); if ( beta > fanAngle - 2*gamma && beta <= 2*fanAngle ) { // in region 2 newdetval[newiDet] += (beta +2*gamma - fanAngle)/(PI+2*gamma) * detval[iDet]; } else if ( beta > 2*fanAngle && beta <= PI - 2*gamma) { // in region 3 newdetval[newiDet] += (beta +2*gamma - fanAngle)/(PI+2*gamma) * detval[iDet]; } else if ( beta > PI -2*gamma && beta <= PI + fanAngle ) { // in region 4 newdetval[newiDet] += (beta +2*gamma - fanAngle)/(PI+2*gamma) * detval[iDet]; } else if ( beta > PI + fanAngle && beta <= PI +2*fanAngle -2*gamma) { // in region 5 newdetval[newiDet] += (2*PI - beta - 2*gamma + fanAngle)/(PI-2*gamma) *detval[iDet]; } else if ( beta > PI +2*fanAngle -2*gamma && beta <= 2*PI) { // in region 6 newdetval[newiDet] += (2*PI - beta - 2*gamma + fanAngle)/(PI-2*gamma) *detval[iDet]; } else if ( beta > 2*PI && beta <= 2*PI + fanAngle -2*gamma){ // in region 7 newdetval[newiDet] += (2*PI - beta -2*gamma + fanAngle)/(PI-2*gamma) *detval[iDet]; } else { ; // outside region of interest } } } } deleteProjData(); m_projData = newdetarray; m_nView = last_interp_view+1; return (0); } // HalfScanFeather: // A HalfScan Projection Data Set for equiangular geometry, // covering gantry angles from 0 to pi+fanBeamAngle // and fan angle gamma from -fanBeamAngle/2 to fanBeamAngle/2 // contains redundant information. If one copy of this data is left as // zero, (as in the Helical180LI routine above) overweighting is avoided, // but the discontinuity in the data introduces ringing in the image. // This routine makes a copy of the data and applies a weighting to avoid // over-representation, as given in Appendix C of Crawford and King, Med // Phys 17 1990, p967. int Projections::HalfScanFeather(void) { double dbeta = m_rotInc; double dgamma = m_detInc; double fanAngle = m_dFanBeamAngle; // is there enough data? if ( m_nView != static_cast(( PI+fanAngle ) / dbeta) +1 ){ std::cerr << "Data set does seem have enough data to be a halfscan data set" << std::endl; return (1); } if (m_geometry == Scanner::GEOMETRY_INVALID) { std::cerr << "Invalid geometry " << m_geometry << std::endl; return (2); } if (m_geometry == Scanner::GEOMETRY_PARALLEL) { std::cerr << "HalfScanFeather not yet implemented for PARALLEL geometry"<< std::endl; return (2); } for ( int iView2 = 0 ; iView2 < m_nView; iView2++) { double beta2 = iView2 * dbeta; for ( int iDet2 = 0; iDet2 < m_nDet; iDet2++) { double gamma2 = (iDet2 -(m_nDet-1)/2)* dgamma ; if ( ( beta2 >= PI - 2*gamma2) ) { // in redundant data region int iView1, iDet1; iDet1 = (m_nDet -1) - iDet2; //iView1 = nearest((beta2 + 2*gamma2 - PI)/dbeta); iView1 = nearest(( (iView2*dbeta) + 2*(iDet2-(m_nDet-1)/2)*dgamma - PI)/dbeta); const std::span detval2 = (m_projData[iView2])->detValues(); const std::span detval1 = (m_projData[iView1])->detValues(); detval1[iDet1] = detval2[iDet2] ; double x, w1,w2,beta1, gamma1; beta1= iView1*dbeta; gamma1 = -gamma2; if ( beta1 <= (fanAngle - 2*gamma1) ) x = beta1 / ( fanAngle - 2*gamma1); else if ( (fanAngle - 2*gamma1 <= beta1 ) && beta1 <= PI - 2*gamma1) x = 1; else if ( (PI - 2*gamma1 <= beta1 ) && ( beta1 <=PI + fanAngle) ) x = (PI +fanAngle - beta1)/(fanAngle + 2*gamma1); else { std::cerr << "Shouldn't be here!"<< std::endl; return(4); } w1 = (3*x - 2*x*x)*x; w2 = 1-w1; detval1[iDet1] *= w1; detval2[iDet2] *= w2; } } } // heuristic scaling, why this factor? double scalefactor = m_nView * m_rotInc / PI; for ( int iView = 0 ; iView < m_nView; iView++) { const std::span detval = (m_projData[iView])->detValues(); for ( int iDet = 0; iDet < m_nDet; iDet++) { detval[iDet] *= scalefactor; } } return (0); } // NAME // newProjData void Projections::newProjData (void) { if (m_projData) sys_error (ERR_WARNING, "m_projData != NULL [newProjData]"); if (m_nView > 0 && m_nDet) { m_projData = new DetectorArray* [m_nView]; for (int i = 0; i < m_nView; i++) m_projData[i] = new DetectorArray (m_nDet); } } /* NAME * projections_free Free memory allocated to projections * * SYNOPSIS * projections_free(proj) * Projections& proj Projectionss to be deallocated */ void Projections::deleteProjData (void) { if (m_projData != nullptr) { for (int i = 0; i < m_nView; i++) delete m_projData[i]; delete [] m_projData; // allocated with new DetectorArray* [m_nView] m_projData = nullptr; } } /* NAME * Projections::headerWwrite Write data header for projections file * */ bool Projections::headerWrite (fnetorderstream& fs) { kuint16 _hsize = m_headerSize; kuint16 _signature = m_signature; kuint32 _nView = m_nView; kuint32 _nDet = m_nDet; kuint32 _geom = m_geometry; kuint16 _remarksize = m_remark.length(); kuint16 _year = m_year; kuint16 _month = m_month; kuint16 _day = m_day; kuint16 _hour = m_hour; kuint16 _minute = m_minute; kuint16 _second = m_second; kfloat64 _calcTime = m_calcTime; kfloat64 _rotStart = m_rotStart; kfloat64 _rotInc = m_rotInc; kfloat64 _detStart = m_detStart; kfloat64 _detInc = m_detInc; kfloat64 _viewDiameter = m_dViewDiameter; kfloat64 _focalLength = m_dFocalLength; kfloat64 _sourceDetectorLength = m_dSourceDetectorLength; kfloat64 _fanBeamAngle = m_dFanBeamAngle; fs.seekp(0); if (! fs) return false; fs.writeInt16 (_hsize); fs.writeInt16 (_signature); fs.writeInt32 (_nView); fs.writeInt32 (_nDet); fs.writeInt32 (_geom); fs.writeFloat64 (_calcTime); fs.writeFloat64 (_rotStart); fs.writeFloat64 (_rotInc); fs.writeFloat64 (_detStart); fs.writeFloat64 (_detInc); fs.writeFloat64 (_viewDiameter); fs.writeFloat64 (_focalLength); fs.writeFloat64 (_sourceDetectorLength); fs.writeFloat64 (_fanBeamAngle); fs.writeInt16 (_year); fs.writeInt16 (_month); fs.writeInt16 (_day); fs.writeInt16 (_hour); fs.writeInt16 (_minute); fs.writeInt16 (_second); fs.writeInt16 (_remarksize); fs.write (m_remark.c_str(), _remarksize); m_headerSize = fs.tellp(); _hsize = m_headerSize; fs.seekp(0); fs.writeInt16 (_hsize); if (! fs) return false; return true; } /* NAME * projections_read_header Read data header for projections file * */ bool Projections::headerRead (fnetorderstream& fs) { kuint16 _hsize, _signature, _year, _month, _day, _hour, _minute, _second, _remarksize = 0; kuint32 _nView, _nDet, _geom; kfloat64 _calcTime, _rotStart, _rotInc, _detStart, _detInc, _focalLength, _sourceDetectorLength, _viewDiameter, _fanBeamAngle; fs.seekg(0); if (! fs) return false; fs.readInt16 (_hsize); fs.readInt16 (_signature); fs.readInt32 (_nView); fs.readInt32 (_nDet); fs.readInt32 (_geom); fs.readFloat64 (_calcTime); fs.readFloat64 (_rotStart); fs.readFloat64 (_rotInc); fs.readFloat64 (_detStart); fs.readFloat64 (_detInc); fs.readFloat64 (_viewDiameter); fs.readFloat64 (_focalLength); fs.readFloat64 (_sourceDetectorLength); fs.readFloat64 (_fanBeamAngle); fs.readInt16 (_year); fs.readInt16 (_month); fs.readInt16 (_day); fs.readInt16 (_hour); fs.readInt16 (_minute); fs.readInt16 (_second); fs.readInt16 (_remarksize); if (! fs) { sys_error (ERR_SEVERE, "Error reading header information , _remarksize={} [projections_read_header]", _remarksize); return false; } if (_signature != m_signature) { sys_error (ERR_SEVERE, "File {} does not have a valid projection file signature", m_filename); return false; } std::vector vecRemarkStorage (_remarksize + 1); char* pszRemarkStorage = vecRemarkStorage.data(); fs.read (pszRemarkStorage, _remarksize); if (! fs) { sys_error (ERR_SEVERE, "Error reading remark, _remarksize = {}", _remarksize); return false; } pszRemarkStorage[_remarksize] = 0; m_remark = pszRemarkStorage; std::streamoff _hsizeread = fs.tellg(); if (!fs || _hsizeread != _hsize) { sys_error (ERR_WARNING, "File header size read {} != file header size stored {} [read_projections_header]\n_remarksize={}", (long int) _hsizeread, _hsize, _remarksize); return false; } m_headerSize = _hsize; m_nView = _nView; m_nDet = _nDet; m_geometry = _geom; m_calcTime = _calcTime; m_rotStart = _rotStart; m_rotInc = _rotInc; m_detStart = _detStart; m_detInc = _detInc; m_dFocalLength = _focalLength; m_dSourceDetectorLength = _sourceDetectorLength; m_dViewDiameter = _viewDiameter; m_dFanBeamAngle = _fanBeamAngle; m_year = _year; m_month = _month; m_day = _day; m_hour = _hour; m_minute = _minute; m_second = _second; m_label.setLabelType (Array2dFileLabel::L_HISTORY); m_label.setLabelString (m_remark); m_label.setCalcTime (m_calcTime); m_label.setDateTime (m_year, m_month, m_day, m_hour, m_minute, m_second); return true; } bool Projections::read (const std::string& filename) { return read (filename.c_str()); } bool Projections::read (const char* filename) { if (ctsimFileIsHDF5 (filename)) { if (! ctsimHaveHDF5()) { sys_error (ERR_SEVERE, "{} is an HDF5 file but this build has no HDF5 support", filename); return false; } return readHDF5 (filename); } m_filename = filename; #ifdef MSVC frnetorderstream fileRead (m_filename.c_str(), std::ios::in | std::ios::binary); #else frnetorderstream fileRead (m_filename.c_str(), std::ios::in | std::ios::binary); // | std::ios::nocreate); #endif if (fileRead.fail()) return false; if (! headerRead (fileRead)) return false; deleteProjData (); newProjData(); for (int i = 0; i < m_nView; i++) { if (! detarrayRead (fileRead, *m_projData[i], i)) break; } fileRead.close(); return true; } bool Projections::copyViewData (std::string_view filename, std::ostream& os, int startView, int endView) { frnetorderstream is (std::string (filename).c_str(), std::ios::in | std::ios::binary); kuint16 sizeHeader, signature; kuint32 _nView, _nDet; is.seekg (0); if (is.fail()) { sys_error (ERR_SEVERE, "Unable to read projection file {}", filename); return false; } is.readInt16 (sizeHeader); is.readInt16 (signature); is.readInt32 (_nView); is.readInt32 (_nDet); int nView = _nView; int nDet = _nDet; if (signature != m_signature) { sys_error (ERR_SEVERE, "Illegal signature in projection file {}", filename); return false; } if (startView < 0) startView = 0; if (startView > nView - 1) startView = nView; if (endView < 0 || endView > nView - 1) endView = nView - 1; if (startView > endView) { // swap if start > end int tempView = endView; endView = startView; startView = tempView; } int sizeView = 8 /* view_angle */ + 4 /* nDet */ + (4 * nDet); unsigned char* pViewData = new unsigned char [sizeView]; for (int i = startView; i <= endView; i++) { is.seekg (sizeHeader + i * sizeView); is.read (reinterpret_cast(pViewData), sizeView); os.write (reinterpret_cast(pViewData), sizeView); if (is.fail() || os.fail()) break; } delete [] pViewData; if (is.fail()) sys_error (ERR_SEVERE, "Error reading projection file"); if (os.fail()) sys_error (ERR_SEVERE, "Error writing projection file"); return (! (is.fail() || os.fail())); } bool Projections::copyHeader (std::string_view filename, std::ostream& os) { frnetorderstream is (std::string (filename).c_str(), std::ios::in | std::ios::binary); kuint16 sizeHeader, signature; is.readInt16 (sizeHeader); is.readInt16 (signature); is.seekg (0); if (signature != m_signature) { sys_error (ERR_SEVERE, "Illegal signature in projection file {}", filename); return false; } unsigned char* pHdrData = new unsigned char [sizeHeader]; is.read (reinterpret_cast(pHdrData), sizeHeader); if (is.fail()) { sys_error (ERR_SEVERE, "Error reading header"); return false; } os.write (reinterpret_cast(pHdrData), sizeHeader); if (os.fail()) { sys_error (ERR_SEVERE, "Error writing header"); return false; } return true; } bool Projections::write (const std::string& filename) { return write (filename.c_str()); } bool Projections::write (const char* filename) { if (ctsimDefaultFileFormat() == CTSIM_FORMAT_HDF5 && ctsimHaveHDF5()) return writeHDF5 (filename); frnetorderstream fs (std::string (filename).c_str(), std::ios::out | std::ios::binary | std::ios::trunc | std::ios::ate); m_filename = filename; if (! fs) { sys_error (ERR_SEVERE, "Error opening file {} for output [projections_create]", filename); return false; } if (! headerWrite (fs)) return false; if (m_projData != nullptr) { for (int i = 0; i < m_nView; i++) { if (! detarrayWrite (fs, *m_projData[i], i)) break; } } if (! fs) return false; fs.close(); return true; } /* NAME * detarrayRead Read a Detector Array structure from the disk * * SYNOPSIS * detarrayRead (proj, darray, view_num) * DETARRAY *darray Detector array storage location to be filled * int view_num View number to read */ bool Projections::detarrayRead (fnetorderstream& fs, DetectorArray& darray, const int iview) { const int detval_bytes = darray.nDet() * sizeof(kfloat32); const int detheader_bytes = sizeof(kfloat64) /* view_angle */ + sizeof(kint32) /* nDet */; const int view_bytes = detheader_bytes + detval_bytes; const std::streamoff start_data = m_headerSize + (iview * view_bytes); const std::span detval_ptr = darray.detValues(); kfloat64 view_angle; kuint32 nDet; fs.seekg (start_data); fs.readFloat64 (view_angle); fs.readInt32 (nDet); darray.setViewAngle (view_angle); // darray.setNDet ( nDet); for (unsigned int i = 0; i < nDet; i++) { kfloat32 detval; fs.readFloat32 (detval); detval_ptr[i] = detval; } if (! fs) return false; return true; } /* NAME * detarrayWrite Write detector array data to the disk * * SYNOPSIS * detarrayWrite (darray, view_num) * DETARRAY *darray Detector array data to be written * int view_num View number to write * * DESCRIPTION * This routine writes the detarray data from the disk sequentially to * the file that was opened with open_projections(). Data is written in * binary format. */ bool Projections::detarrayWrite (fnetorderstream& fs, const DetectorArray& darray, const int iview) { const int detval_bytes = darray.nDet() * sizeof(float); const int detheader_bytes = sizeof(kfloat64) /* view_angle */ + sizeof(kint32) /* nDet */; const int view_bytes = detheader_bytes + detval_bytes; const std::streamoff start_data = m_headerSize + (iview * view_bytes); const std::span detval_ptr = darray.detValues(); kfloat64 view_angle = darray.viewAngle(); kuint32 nDet = darray.nDet(); fs.seekp (start_data); if (! fs) { sys_error (ERR_SEVERE, "Error seeking detectory array [detarrayWrite]"); return false; } fs.writeFloat64 (view_angle); fs.writeInt32 (nDet); for (unsigned int i = 0; i < nDet; i++) { kfloat32 detval = detval_ptr[i]; fs.writeFloat32 (detval); } if (! fs) return (false); return true; } /* NAME * printProjectionData Print projections data * * SYNOPSIS * printProjectionData () */ void Projections::printProjectionData () { printProjectionData (0, nView() - 1); } void Projections::printProjectionData (int startView, int endView) { printf("Projections Data\n\n"); printf("Description: %s\n", m_remark.c_str()); printf("Geometry: %s\n", Scanner::convertGeometryIDToName (m_geometry)); printf("nView = %8d nDet = %8d\n", m_nView, m_nDet); printf("focalLength = %8.4f ViewDiameter = %8.4f\n", m_dFocalLength, m_dViewDiameter); printf("fanBeamAngle= %8.4f SourceDetector = %8.4f\n", convertRadiansToDegrees(m_dFanBeamAngle), m_dSourceDetectorLength); printf("rotStart = %8.4f rotInc = %8.4f\n", m_rotStart, m_rotInc); printf("detStart = %8.4f detInc = %8.4f\n", m_detStart, m_detInc); if (m_projData != nullptr) { if (startView < 0) startView = 0; if (endView < 0) endView = m_nView - 1; if (startView > m_nView - 1) startView = m_nView - 1; if (endView > m_nView - 1) endView = m_nView - 1; for (int ir = startView; ir <= endView - 1; ir++) { printf("View %d: angle %f\n", ir, m_projData[ir]->viewAngle()); const std::span detval = m_projData[ir]->detValues(); for (int id = 0; id < m_projData[ir]->nDet(); id++) printf("%8.4f ", detval[id]); printf("\n"); } } } void Projections::printScanInfo (std::ostringstream& os) const { os << "Number of detectors: " << m_nDet << "\n"; os << "Number of views: " << m_nView<< "\n"; os << "Description: " << m_remark.c_str()<< "\n"; os << "Geometry: " << Scanner::convertGeometryIDToName (m_geometry)<< "\n"; os << "Focal Length: " << m_dFocalLength<< "\n"; os << "Source Detector Length: " << m_dSourceDetectorLength << "\n"; os << "View Diameter: " << m_dViewDiameter<< "\n"; os << "Fan Beam Angle: " << convertRadiansToDegrees(m_dFanBeamAngle) << "\n"; os << "detStart: " << m_detStart<< "\n"; os << "detInc: " << m_detInc<< "\n"; os << "rotStart: " << m_rotStart<< "\n"; os << "rotInc: " << m_rotInc<< "\n"; } bool Projections::convertPolar (ImageFile& rIF, int iInterpolationID) { unsigned int nx = rIF.nx(); unsigned int ny = rIF.ny(); ImageFileArray v = rIF.getArray(); ImageFileArray vImag = rIF.getImaginaryArray(); if (v.empty() || nx == 0 || ny == 0) return false; Projections* pProj = this; if (m_geometry == Scanner::GEOMETRY_EQUIANGULAR || m_geometry == Scanner::GEOMETRY_EQUILINEAR) pProj = interpolateToParallel(); Array2d adView (nx, ny); Array2d adDet (nx, ny); double** ppdView = adView.getArray(); double** ppdDet = adDet.getArray(); std::vector>> ppcDetValue (pProj->m_nView); int iView; for (iView = 0; iView < pProj->m_nView; iView++) { ppcDetValue[iView].assign (pProj->m_nDet, std::complex (0., 0.)); const std::span detval = pProj->getDetectorArray (iView).detValues(); for (int iDet = 0; iDet < pProj->m_nDet; iDet++) ppcDetValue[iView][iDet] = std::complex(detval[iDet], 0); } pProj->calcArrayPolarCoordinates (nx, ny, ppdView, ppdDet, pProj->m_nDet, 1., pProj->m_detInc); // interpolatePolar takes a table of row pointers; the rows live in the // vector above, so the table is just a view of them. std::vector*> vecDetRows (ppcDetValue.size()); for (std::size_t i = 0; i < ppcDetValue.size(); i++) vecDetRows[i] = ppcDetValue[i].data(); pProj->interpolatePolar (v, vImag, nx, ny, vecDetRows.data(), ppdView, ppdDet, pProj->m_nView, pProj->m_nDet, pProj->m_nDet, iInterpolationID); if (m_geometry == Scanner::GEOMETRY_EQUIANGULAR || m_geometry == Scanner::GEOMETRY_EQUILINEAR) delete pProj; return true; } bool Projections::convertFFTPolar (ImageFile& rIF, int iInterpolationID, int iZeropad) { #ifndef HAVE_FFTW rIF.arrayDataClear(); return false; #else unsigned int nx = rIF.nx(); unsigned int ny = rIF.ny(); ImageFileArray v = rIF.getArray(); if (! rIF.isComplex()) rIF.convertRealToComplex(); ImageFileArray vImag = rIF.getImaginaryArray(); if (v.empty() || nx == 0 || ny == 0) return false; Projections* pProj = this; if (m_geometry == Scanner::GEOMETRY_EQUIANGULAR || m_geometry == Scanner::GEOMETRY_EQUILINEAR) pProj = interpolateToParallel(); int iInterpDet = static_cast(static_cast(sqrt(nx*nx+ny*ny))); int iNumInterpDetWithZeros = ProcessSignal::addZeropadFactor (iInterpDet, iZeropad); fftw_complex* pcIn = static_cast (fftw_malloc (sizeof(fftw_complex) * iNumInterpDetWithZeros)); fftw_plan plan = fftw_plan_dft_1d (iNumInterpDetWithZeros, pcIn, pcIn, FFTW_FORWARD, FFTW_ESTIMATE); std::vector>> ppcDetValue (pProj->m_nView); //double dInterpScale = (pProj->m_nDet-1) / static_cast(iInterpDet-1); double dInterpScale = pProj->m_nDet / static_cast(iInterpDet); // Sample spacing of the interpolated projections. The central-slice theorem // gives P(nu) = integral p(t) exp(-i2*pi*nu*t) dt, which the DFT approximates // as dDeltaT * sum(...). The scale is the sample spacing and nothing else: // in particular it must not depend on the transform length, or zero-padding // (which only interpolates the spectrum) would change the amplitude. Before // 6.3.1 this was iInterpDet/(viewDiameter*0.05) divided by iInterpDet squared, // which was both geometry dependent and padding dependent. const double dDeltaT = pProj->m_detInc * dInterpScale; const double dFFTScale = dDeltaT; int iMidPoint = iInterpDet / 2; double dMidPoint = static_cast(iInterpDet) / 2.; int iZerosAdded = iNumInterpDetWithZeros - iInterpDet; // For each view, interpolate, shift to center at origin, and FFT for (int iView = 0; iView < m_nView; iView++) { const std::span detval = pProj->getDetectorArray(iView).detValues(); LinearInterpolator projInterp (detval.data(), pProj->m_nDet); for (int iDet = 0; iDet < iInterpDet; iDet++) { double dInterpPos = (m_nDet / 2.) + (iDet - dMidPoint) * dInterpScale; pcIn[iDet][0] = projInterp.interpolate (dInterpPos); pcIn[iDet][1] = 0; } Fourier::shuffleFourierToNaturalOrder (pcIn, iInterpDet); if (iZerosAdded > 0) { for (int iDet1 = iInterpDet -1; iDet1 >= iMidPoint; iDet1--) { pcIn[iDet1+iZerosAdded][0] = pcIn[iDet1][0]; pcIn[iDet1+iZerosAdded][1] = pcIn[iDet1][1]; } for (int iDet2 = iMidPoint; iDet2 < iMidPoint + iZerosAdded; iDet2++) pcIn[iDet2][0] = pcIn[iDet2][1] = 0; } fftw_execute (plan); ppcDetValue[iView].assign (iNumInterpDetWithZeros, std::complex (0., 0.)); for (int iD = 0; iD < iNumInterpDetWithZeros; iD++) { ppcDetValue[iView][iD] = std::complex (pcIn[iD][0] * dFFTScale, pcIn[iD][1] * dFFTScale); } Fourier::shuffleFourierToNaturalOrder (ppcDetValue[iView].data(), iNumInterpDetWithZeros); } fftw_free(pcIn) ; fftw_destroy_plan (plan); Array2d adView (nx, ny); Array2d adDet (nx, ny); double** ppdView = adView.getArray(); double** ppdDet = adDet.getArray(); // The reconstruction field is proj.phmLen(), which is what Backproject uses // for xMin/xMax; viewDiameter is the scan circle and is larger. pProj->calcArrayFrequencyCoordinates (nx, ny, ppdView, ppdDet, iNumInterpDetWithZeros, dDeltaT, pProj->phmLen()); // interpolatePolar takes a table of row pointers; the rows live in the // vector above, so the table is just a view of them. std::vector*> vecDetRows (ppcDetValue.size()); for (std::size_t i = 0; i < ppcDetValue.size(); i++) vecDetRows[i] = ppcDetValue[i].data(); pProj->interpolatePolar (v, vImag, nx, ny, vecDetRows.data(), ppdView, ppdDet, pProj->m_nView, pProj->m_nDet, iNumInterpDetWithZeros, iInterpolationID); if (m_geometry == Scanner::GEOMETRY_EQUIANGULAR || m_geometry == Scanner::GEOMETRY_EQUILINEAR) delete pProj; return true; #endif } void Projections::calcArrayFrequencyCoordinates (unsigned int nx, unsigned int ny, double** ppdView, double** ppdDet, int iNumDetWithZeros, double dDeltaT, double dFOV) const { // The output grid holds the 2-D transform of an image covering dFOV with // nx by ny pixels, so its frequency spacing is 1/dFOV and DC sits at the // centre pixel (which is where shuffleFourierToNaturalOrder puts index 0). const double dImageDeltaNu = 1. / dFOV; // Frequency spacing of the transformed projections. Zero-padding makes this // finer, which is exactly what padding is for: it interpolates the spectrum. const double dDetDeltaNu = 1. / (iNumDetWithZeros * dDeltaT); // DC of a shuffled even-length transform sits at n/2, not (n-1)/2. Using the // latter, as releases before 6.3.1 did, offsets the radial axis by half a // sample: a linear phase error across every projection. const double dDetCenter = isEven (iNumDetWithZeros) ? iNumDetWithZeros / 2. : (iNumDetWithZeros - 1) / 2.; // The pixel that holds DC. shuffleFourierToNaturalOrder puts DC at // n/2 for an even n and at (n-1)/2 for an odd one, so this must agree // or the whole spectrum is half a bin off: for an odd nx, nx / 2. falls // between two pixels, leaving none at zero frequency and every other // frequency carrying the phase ramp of a half-bin shift. const double dImageCenterX = isEven (nx) ? nx / 2. : (nx - 1) / 2.; const double dImageCenterY = isEven (ny) ? ny / 2. : (ny - 1) / 2.; for (unsigned int ix = 0; ix < nx; ix++) { const double u = (static_cast(ix) - dImageCenterX) * dImageDeltaNu; for (unsigned int iy = 0; iy < ny; iy++) { const double v = (static_cast(iy) - dImageCenterY) * dImageDeltaNu; double dNu = ::sqrt (u * u + v * v); double phi = atan2 (v, u); if (phi <= -m_rotInc / 2) phi += TWOPI; if (phi >= PI - (m_rotInc / 2)) { // views span 0..PI; use a negative radius phi -= PI; dNu = -dNu; } ppdView[ix][iy] = (phi - m_rotStart) / m_rotInc; ppdDet[ix][iy] = (dNu / dDetDeltaNu) + dDetCenter; } } } bool Projections::reconstructFourier (ImageFile& rIF, int iInterpolationID, int iZeropad) { if (! convertFFTPolar (rIF, iInterpolationID, iZeropad)) return false; // ImageFile::ifft already shuffles its input from centred to transform order, // so the centred spectrum from convertFFTPolar is fed to it directly; only the // spatial result has to be re-centred afterwards. Keeping the phase this way // means the real part is the image and no magnitude step is needed, so // negative values survive instead of being folded positive. rIF.ifft (rIF); Fourier::shuffleFourierToNaturalOrder (rIF); // ImageFile::ifft is an unnormalized sum, so f(x,y) = dNu^2 * sum(...) with // dNu = 1/FOV. The real part is the image; the imaginary part is // interpolation error and is worth keeping as a diagnostic. const double dFOV = phmLen(); const double dScale = 1. / (dFOV * dFOV); ImageFileArray v = rIF.getArray(); for (unsigned int ix = 0; ix < rIF.nx(); ix++) for (unsigned int iy = 0; iy < rIF.ny(); iy++) v[ix][iy] *= dScale; return true; } void Projections::calcArrayPolarCoordinates (unsigned int nx, unsigned int ny, double** ppdView, double** ppdDet, int iNumDetWithZeros, double dZeropadRatio, double dDetInc) { double dLength = viewDiameter(); double xMin = -dLength / 2; double xMax = xMin + dLength; double yMin = -dLength / 2; double yMax = yMin + dLength; double xCent = (xMin + xMax) / 2; double yCent = (yMin + yMax) / 2; xMin = (xMin - xCent) * dZeropadRatio + xCent; xMax = (xMax - xCent) * dZeropadRatio + xCent; yMin = (yMin - yCent) * dZeropadRatio + yCent; yMax = (yMax - yCent) * dZeropadRatio + yCent; double xInc = (xMax - xMin) / nx; // size of cells double yInc = (yMax - yMin) / ny; double dDetCenter = (iNumDetWithZeros - 1) / 2.; // index refering to L=0 projection // +1 is correct for frequency data, ndet-1 is correct for projections // if (isEven (iNumDetWithZeros)) // dDetCenter = (iNumDetWithZeros + 0) / 2; // Calculates polar coordinates (view#, det#) for each point on phantom grid double x = xMin + xInc / 2; // Rectang coords of center of pixel for (unsigned int ix = 0; ix < nx; ix++, x += xInc) { double y = yMin + yInc / 2; for (unsigned int iy = 0; iy < ny; iy++, y += yInc) { double r = ::sqrt (x * x + y * y); double phi = atan2 (y, x); if (phi <= -m_rotInc / 2) phi += TWOPI; if (phi >= PI - (m_rotInc / 2)) { phi -= PI; r = -r; } ppdView[ix][iy] = (phi - m_rotStart) / m_rotInc; ppdDet[ix][iy] = (r / dDetInc) + dDetCenter; } } } void Projections::interpolatePolar (ImageFileArray& v, ImageFileArray& vImag, unsigned int nx, unsigned int ny, std::complex** ppcDetValue, double** ppdView, double** ppdDet, unsigned int nView, unsigned int nDet, unsigned int nDetWithZeros, int iInterpolationID) { using complexValue = std::complex; BilinearPolarInterpolator* pBilinear = nullptr; BicubicPolyInterpolator* pBicubic = nullptr; if (iInterpolationID == POLAR_INTERP_BILINEAR) pBilinear = new BilinearPolarInterpolator (ppcDetValue, nView, nDetWithZeros); else if (iInterpolationID == POLAR_INTERP_BICUBIC) pBicubic = new BicubicPolyInterpolator (ppcDetValue, nView, nDetWithZeros); for (unsigned int ix = 0; ix < nx; ix++) { // was bounded by ny for (unsigned int iy = 0; iy < ny; iy++) { if (iInterpolationID == POLAR_INTERP_NEAREST) { unsigned int iView = nearest (ppdView[ix][iy]); unsigned int iDet = nearest (ppdDet[ix][iy]); if (iView == nView) iView = 0; if (iDet < nDetWithZeros && iView < nView) { // iDet/iView are unsigned v[ix][iy] = ppcDetValue[iView][iDet].real(); if (! vImag.empty()) vImag[ix][iy] = ppcDetValue[iView][iDet].imag(); } else { v[ix][iy] = 0; if (! vImag.empty()) vImag[ix][iy] = 0; } } else if (iInterpolationID == POLAR_INTERP_BILINEAR) { std::complex vInterp = pBilinear->interpolate (ppdView[ix][iy], ppdDet[ix][iy]); v[ix][iy] = vInterp.real(); if (! vImag.empty()) vImag[ix][iy] = vInterp.imag(); } else if (iInterpolationID == POLAR_INTERP_BICUBIC) { std::complex vInterp = pBicubic->interpolate (ppdView[ix][iy], ppdDet[ix][iy]); v[ix][iy] = vInterp.real(); if (! vImag.empty()) vImag[ix][iy] = vInterp.imag(); } } } } bool Projections::initFromSomatomAR_STAR (int iNViews, int iNDets, unsigned char* pData, unsigned long lDataLength) { init (iNViews, iNDets); m_geometry = Scanner::GEOMETRY_EQUIANGULAR; m_dFocalLength = 510; m_dSourceDetectorLength = 890; m_detInc = convertDegreesToRadians (3.06976 / 60); m_dFanBeamAngle = iNDets * m_detInc; m_detStart = -(m_dFanBeamAngle / 2); m_rotInc = TWOPI / static_cast(iNViews); m_rotStart = 0; m_dViewDiameter = sin (m_dFanBeamAngle / 2) * m_dFocalLength * 2; if (! ((iNViews == 750 && lDataLength == 1560000L) || (iNViews == 950 && lDataLength == 1976000L) || (iNViews == 1500 && lDataLength == 3120000))) return false; static_assert (sizeof(kint32) == 4, "Somatom AR.STAR records hold 32-bit fields"); double dCenter = (iNDets - 1.) / 2.; // change from (Nm+1)/2 because of 0 vs. 1 indexing std::vector vecCosScale (iNDets); double* pdCosScale = vecCosScale.data(); for (int i = 0; i < iNDets; i++) pdCosScale[i] = 1. / cos ((i - dCenter) * m_detInc); std::size_t lDataPos = 0; // a position in a buffer, not a long for (int iv = 0; iv < iNViews; iv++) { unsigned char* pArgBase = pData + lDataPos; unsigned char* p = pArgBase+0; SwapBytes4IfLittleEndian (p); // kint32 lProjNumber = *reinterpret_cast(p); p = pArgBase+20; SwapBytes4IfLittleEndian (p); // these are 32-bit fields. Reading them through long* read 8 bytes // on LP64, so every scale factor was garbage and all raysums came out // infinite; the Somatom import had never worked on a 64-bit build. kint32 lEscale = *reinterpret_cast(p); p = pArgBase+28; SwapBytes4IfLittleEndian (p); // kint32 lTime = *reinterpret_cast(p); p = pArgBase + 4; SwapBytes4IfLittleEndian (p); double dAlpha = *reinterpret_cast(p) + HALFPI; p = pArgBase+12; SwapBytes4IfLittleEndian (p); // double dAlign = *reinterpret_cast(p); p = pArgBase + 16; SwapBytes4IfLittleEndian (p); // double dMaxValue = *reinterpret_cast(p); DetectorArray& detArray = getDetectorArray (iv); detArray.setViewAngle (dAlpha); const std::span detval = detArray.detValues(); double dViewScale = 1. / (2294.4871 * ::pow (2.0, -static_cast(lEscale))); lDataPos += 32; for (int id = 0; id < iNDets; id++) { // Two bytes, big-endian, as a signed 16-bit sample. The bounds were // written out as 32767 and 65536; the type says what it is. const int iRaw = pData[lDataPos+1] + (pData[lDataPos] << 8); int iV = iRaw; if (iV > std::numeric_limits::max()) iV -= (static_cast (std::numeric_limits::max()) + 1); detval[id] = iV * dViewScale * pdCosScale[id]; lDataPos += 2; } #if 1 for (int k = iNDets - 2; k >= 0; k--) detval[k+1] = detval[k]; detval[0] = 0; #endif } return true; } Projections* Projections::interpolateToParallel () const { if (m_geometry == Scanner::GEOMETRY_PARALLEL) return const_cast(this); // Timed: rebinning is one of two ways to reconstruct a divergent scan, // and what it costs is worth comparing. Timer timerRebin; int nDet = m_nDet; int nView = m_nView; Projections* pProjNew = new Projections (nView, nDet); pProjNew->m_geometry = Scanner::GEOMETRY_PARALLEL; pProjNew->m_dFocalLength = m_dFocalLength; pProjNew->m_dSourceDetectorLength = m_dSourceDetectorLength; pProjNew->m_dViewDiameter = m_dViewDiameter; pProjNew->m_dFanBeamAngle = m_dFanBeamAngle; pProjNew->m_remark = m_remark; pProjNew->m_remark += "; Interpolate to Parallel"; pProjNew->m_label.setLabelType (Array2dFileLabel::L_HISTORY); pProjNew->m_label.setLabelString (pProjNew->m_remark); pProjNew->m_label.setDateTime (pProjNew->m_year, pProjNew->m_month, pProjNew->m_day, pProjNew->m_hour, pProjNew->m_minute, pProjNew->m_second); pProjNew->m_rotStart = 0; #ifdef CONVERT_PARALLEL_PI pProjNew->m_rotInc = PI / nView;; #else pProjNew->m_rotInc = TWOPI / nView; #endif pProjNew->m_detStart = -m_dViewDiameter / 2; pProjNew->m_detInc = m_dViewDiameter / nDet; if (isEven (nDet)) // even pProjNew->m_detInc = m_dViewDiameter / (nDet - 1); ParallelRaysums parallel (this, ParallelRaysums::THETA_RANGE_NORMALIZE_TO_TWOPI); std::vector vecThetaValuesForT (pProjNew->nView()); double* pdThetaValuesForT = vecThetaValuesForT.data(); std::vector vecRaysumsForT (pProjNew->nView()); double* pdRaysumsForT = vecRaysumsForT.data(); // interpolate to evenly spaced theta (views) for (int iD = 0; iD < pProjNew->nDet(); iD++) { parallel.getThetaAndRaysumsForT (iD, pdThetaValuesForT, pdRaysumsForT); double dViewAngle = m_rotStart; int iLastFloor = -1; LinearInterpolator interp (pdThetaValuesForT, pdRaysumsForT, pProjNew->nView(), false); for (int iV = 0; iV < pProjNew->nView(); iV++, dViewAngle += pProjNew->m_rotInc) { const std::span detValues = pProjNew->getDetectorArray (iV).detValues(); detValues[iD] = interp.interpolate (dViewAngle, &iLastFloor); } } // interpolate to evenly space t (detectors) std::vector vecOriginalDetPositions (pProjNew->nDet()); double* pdOriginalDetPositions = vecOriginalDetPositions.data(); parallel.getDetPositions (pdOriginalDetPositions); std::vector vecDetValueCopy (pProjNew->nDet()); double* pdDetValueCopy = vecDetValueCopy.data(); double dViewAngle = m_rotStart; for (int iV = 0; iV < pProjNew->nView(); iV++, dViewAngle += pProjNew->m_rotInc) { DetectorArray& detArray = pProjNew->getDetectorArray (iV); const std::span detValues = detArray.detValues(); detArray.setViewAngle (dViewAngle); for (int i = 0; i < pProjNew->nDet(); i++) pdDetValueCopy[i] = detValues[i]; double dDetPos = pProjNew->m_detStart; int iLastFloor = -1; LinearInterpolator interp (pdOriginalDetPositions, pdDetValueCopy, pProjNew->nDet(), false); for (int iD = 0; iD < pProjNew->nDet(); iD++, dDetPos += pProjNew->m_detInc) detValues[iD] = interp.interpolate (dDetPos, &iLastFloor); } // Stopped here, after both passes: the time belongs to the interpolation // and not to setting up the result. pProjNew->m_calcTime = timerRebin.timerEnd(); pProjNew->m_label.setCalcTime (pProjNew->m_calcTime); return pProjNew; } /////////////////////////////////////////////////////////////////////////////// // // Class ParallelRaysums // // Used for converting divergent beam raysums into Parallel raysums // /////////////////////////////////////////////////////////////////////////////// ParallelRaysums::ParallelRaysums (const Projections* pProjections, int iThetaRange) : m_iNumCoordinates(0), m_iNumView(pProjections->nView()), m_iNumDet(pProjections->nDet()), m_iThetaRange (iThetaRange) { int iGeometry = pProjections->geometry(); double dDetInc = pProjections->detInc(); double dDetStart = pProjections->detStart(); double dFocalLength = pProjections->focalLength(); m_iNumCoordinates = m_iNumView * m_iNumDet; m_vecCoordinates.resize (m_iNumCoordinates); m_vecpCoordinates.resize (m_iNumCoordinates); for (int i = 0; i < m_iNumCoordinates; i++) m_vecpCoordinates[i] = &m_vecCoordinates[i]; int iCoordinate = 0; for (int iV = 0; iV < m_iNumView; iV++) { double dViewAngle = pProjections->getDetectorArray(iV).viewAngle(); const std::span detValues = pProjections->getDetectorArray(iV).detValues(); double dDetPos = dDetStart; for (int iD = 0; iD < m_iNumDet; iD++) { ParallelRaysumCoordinate* pC = m_vecpCoordinates[iCoordinate++]; if (iGeometry == Scanner::GEOMETRY_PARALLEL) { pC->m_dTheta = dViewAngle; pC->m_dT = dDetPos; } else if (iGeometry == Scanner::GEOMETRY_EQUILINEAR) { double dFanAngle = atan (dDetPos / pProjections->sourceDetectorLength()); pC->m_dTheta = dViewAngle + dFanAngle; pC->m_dT = dFocalLength * sin(dFanAngle); } else if (iGeometry == Scanner::GEOMETRY_EQUIANGULAR) { // fan angle is same as dDetPos pC->m_dTheta = dViewAngle + dDetPos; pC->m_dT = dFocalLength * sin (dDetPos); } if (m_iThetaRange != THETA_RANGE_UNCONSTRAINED) { pC->m_dTheta = normalizeAngle (pC->m_dTheta); if (m_iThetaRange == THETA_RANGE_FOLD_TO_PI && pC->m_dTheta >= PI) { pC->m_dTheta -= PI; pC->m_dT = -pC->m_dT; } } pC->m_dRaysum = detValues[iD]; dDetPos += dDetInc; } } } ParallelRaysums::~ParallelRaysums() { } ParallelRaysums::CoordinateContainer& ParallelRaysums::getSortedByTheta() { if (m_vecpSortedByTheta.size() == 0) { m_vecpSortedByTheta.resize (m_iNumCoordinates); for (int i = 0; i < m_iNumCoordinates; i++) m_vecpSortedByTheta[i] = m_vecpCoordinates[i]; std::sort (m_vecpSortedByTheta.begin(), m_vecpSortedByTheta.end(), ParallelRaysumCoordinate::compareByTheta); } return m_vecpSortedByTheta; } ParallelRaysums::CoordinateContainer& ParallelRaysums::getSortedByT() { if (m_vecpSortedByT.size() == 0) { m_vecpSortedByT.resize (m_iNumCoordinates); for (int i = 0; i < m_iNumCoordinates; i++) m_vecpSortedByT[i] = m_vecpCoordinates[i]; std::sort (m_vecpSortedByT.begin(), m_vecpSortedByT.end(), ParallelRaysumCoordinate::compareByT); } return m_vecpSortedByT; } void ParallelRaysums::getLimits (double* dMinT, double* dMaxT, double* dMinTheta, double* dMaxTheta) const { if (m_iNumCoordinates <= 0) return; *dMinT = *dMaxT = m_vecpCoordinates[0]->m_dT; *dMinTheta = *dMaxTheta = m_vecpCoordinates[0]->m_dTheta; for (int i = 0; i < m_iNumCoordinates; i++) { double dT = m_vecpCoordinates[i]->m_dT; double dTheta = m_vecpCoordinates[i]->m_dTheta; if (dT < *dMinT) *dMinT = dT; else if (dT > *dMaxT) *dMaxT = dT; if (dTheta < *dMinTheta) *dMinTheta = dTheta; else if (dTheta > *dMaxTheta) *dMaxTheta = dTheta; } } void ParallelRaysums::getThetaAndRaysumsForT (int iTheta, double* pTheta, double* pRaysum) { const CoordinateContainer& coordsT = getSortedByT(); int iBase = iTheta * m_iNumView; for (int i = 0; i < m_iNumView; i++) { int iPos = iBase + i; pTheta[i] = coordsT[iPos]->m_dTheta; pRaysum[i] = coordsT[iPos]->m_dRaysum; } } void ParallelRaysums::getDetPositions (double* pdDetPos) { const CoordinateContainer& coordsT = getSortedByT(); int iPos = 0; for (int i = 0; i < m_iNumDet; i++) { pdDetPos[i] = coordsT[iPos]->m_dT; iPos += m_iNumView; } } // --------------------------------------------------------------------------- // Gridding (non-uniform FFT) reconstruction -- 6.3.2 // --------------------------------------------------------------------------- // Modified Bessel function of the first kind, order zero. // Abramowitz & Stegun 9.8.1 / 9.8.2; accurate to about 2e-7, far better than // the interpolation error this kernel is there to control. static double besselI0 (double x) { const double ax = fabs (x); if (ax < 3.75) { const double y = (x / 3.75) * (x / 3.75); return 1.0 + y * (3.5156229 + y * (3.0899424 + y * (1.2067492 + y * (0.2659732 + y * (0.0360768 + y * 0.0045813))))); } const double y = 3.75 / ax; return (exp (ax) / sqrt (ax)) * (0.39894228 + y * (0.01328592 + y * (0.00225319 + y * (-0.00157565 + y * (0.00916281 + y * (-0.02057706 + y * (0.02635537 + y * (-0.01647633 + y * 0.00392377)))))))); } // Fourier transform of the Kaiser-Bessel kernel, normalized so c(0) = 1. // dXNorm is position in units of the oversampled grid, i.e. -0.5 .. 0.5. static double kaiserBesselFT (double dXNorm, int iKernelWidth, double dBeta) { const double dArg = PI * iKernelWidth * dXNorm; const double dSq = dArg * dArg - dBeta * dBeta; double dValue; if (dSq > 1E-12) { const double dRoot = sqrt (dSq); dValue = sin (dRoot) / dRoot; } else if (dSq < -1E-12) { const double dRoot = sqrt (-dSq); dValue = sinh (dRoot) / dRoot; } else dValue = 1.; return dValue / (sinh (dBeta) / dBeta); // normalize so c(0) = 1 } bool Projections::reconstructFourierGrid (ImageFile& rIF, int iZeropad, double dOversample, int iKernelWidth) { #ifndef HAVE_FFTW rIF.arrayDataClear(); return false; #else const unsigned int nx = rIF.nx(); const unsigned int ny = rIF.ny(); if (nx == 0 || ny == 0) return false; if (nx != ny) { sys_error (ERR_SEVERE, "Gridding reconstruction requires a square image [reconstructFourierGrid]"); return false; } if (dOversample < 1.) dOversample = 1.; if (iKernelWidth < 2) iKernelWidth = 2; Projections* pProj = this; if (m_geometry == Scanner::GEOMETRY_EQUIANGULAR || m_geometry == Scanner::GEOMETRY_EQUILINEAR) pProj = interpolateToParallel(); const double dFOV = pProj->phmLen(); // ---- 1-D transform of every projection (identical to the interpolation path) const int iInterpDet = static_cast(sqrt (static_cast(nx * nx + ny * ny))); const int iNumDet = ProcessSignal::addZeropadFactor (iInterpDet, iZeropad); const double dInterpScale = pProj->m_nDet / static_cast(iInterpDet); const double dDeltaT = pProj->m_detInc * dInterpScale; const double dDetDeltaNu = 1. / (iNumDet * dDeltaT); const double dMidPoint = static_cast(iInterpDet) / 2.; const int iMidPoint = iInterpDet / 2; const int iZerosAdded = iNumDet - iInterpDet; const double dDetCenter = isEven (iNumDet) ? iNumDet / 2. : (iNumDet - 1) / 2.; fftw_complex* pcIn = static_cast (fftw_malloc (sizeof(fftw_complex) * iNumDet)); fftw_plan planProj = fftw_plan_dft_1d (iNumDet, pcIn, pcIn, FFTW_FORWARD, FFTW_ESTIMATE); std::vector > > vecSpectrum (pProj->m_nView); for (int iView = 0; iView < pProj->m_nView; iView++) { const std::span detval = pProj->getDetectorArray(iView).detValues(); LinearInterpolator projInterp (detval.data(), pProj->m_nDet); for (int iDet = 0; iDet < iInterpDet; iDet++) { const double dInterpPos = (pProj->m_nDet / 2.) + (iDet - dMidPoint) * dInterpScale; pcIn[iDet][0] = projInterp.interpolate (dInterpPos); pcIn[iDet][1] = 0; } Fourier::shuffleFourierToNaturalOrder (pcIn, iInterpDet); if (iZerosAdded > 0) { for (int i = iInterpDet - 1; i >= iMidPoint; i--) { pcIn[i + iZerosAdded][0] = pcIn[i][0]; pcIn[i + iZerosAdded][1] = pcIn[i][1]; } for (int i = iMidPoint; i < iMidPoint + iZerosAdded; i++) pcIn[i][0] = pcIn[i][1] = 0; } fftw_execute (planProj); vecSpectrum[iView].resize (iNumDet); for (int i = 0; i < iNumDet; i++) vecSpectrum[iView][i] = std::complex (pcIn[i][0] * dDeltaT, pcIn[i][1] * dDeltaT); Fourier::shuffleFourierToNaturalOrder (&vecSpectrum[iView][0], iNumDet); } fftw_free (pcIn); fftw_destroy_plan (planProj); // ---- oversampled Cartesian grid int iGrid = static_cast (dOversample * nx + 0.5); if (isOdd (iGrid)) iGrid++; const double dGridAlpha = static_cast(iGrid) / nx; const double dDeltaUg = 1. / (dGridAlpha * dFOV); // grid FOV = alpha * image FOV // Beatty et al. 2005: the kernel shape that minimizes aliasing energy. double dBetaArg = (iKernelWidth / dGridAlpha) * (iKernelWidth / dGridAlpha) * (dGridAlpha - 0.5) * (dGridAlpha - 0.5) - 0.8; if (dBetaArg < 0.) dBetaArg = 0.; const double dBeta = PI * sqrt (dBetaArg); const double dKernelNorm = dBeta / (iKernelWidth * sinh (dBeta)); // so that the kernel integrates to 1 const double dHalfWidth = iKernelWidth / 2.; // Kernel lookup table, sampled finely in units of grid spacing. const int iTableSize = 2048; std::vector vecKernel (iTableSize + 1); for (int i = 0; i <= iTableSize; i++) { const double dU = dHalfWidth * i / static_cast(iTableSize); const double dT = 1. - (2. * dU / iKernelWidth) * (2. * dU / iKernelWidth); vecKernel[i] = dKernelNorm * besselI0 (dBeta * sqrt (dT > 0. ? dT : 0.)); } std::vector vecGridReal (static_cast(iGrid) * iGrid, 0.); std::vector vecGridImag (static_cast(iGrid) * iGrid, 0.); // Density compensation: polar samples cover an area |nu| * dNu * dTheta, so // without this weight the low frequencies (where the polar samples crowd // together) are counted many times over. The central sample stands for the // disc of radius dNu/2, shared among the views. const double dDeltaTheta = PI / pProj->m_nView; const double dDCWeight = PI * (dDetDeltaNu / 2.) * (dDetDeltaNu / 2.) / pProj->m_nView; const double dNuMax = (iGrid / 2.) * dDeltaUg; // highest frequency the grid holds for (int iView = 0; iView < pProj->m_nView; iView++) { const double dTheta = pProj->m_rotStart + iView * pProj->m_rotInc; const double dCos = cos (dTheta), dSin = sin (dTheta); for (int iDet = 0; iDet < iNumDet; iDet++) { const double dNu = (iDet - dDetCenter) * dDetDeltaNu; if (fabs (dNu) > dNuMax) continue; // beyond what the image grid can represent const double dWeight = (fabs (dNu) < dDetDeltaNu / 2.) ? dDCWeight : fabs (dNu) * dDetDeltaNu * dDeltaTheta; const std::complex cValue = vecSpectrum[iView][iDet] * dWeight; if (cValue.real() == 0. && cValue.imag() == 0.) continue; const double dGu = (dNu * dCos) / dDeltaUg + iGrid / 2.; const double dGv = (dNu * dSin) / dDeltaUg + iGrid / 2.; const int iGuLo = static_cast (ceil (dGu - dHalfWidth)); const int iGuHi = static_cast (floor (dGu + dHalfWidth)); const int iGvLo = static_cast (ceil (dGv - dHalfWidth)); const int iGvHi = static_cast (floor (dGv + dHalfWidth)); for (int iu = iGuLo; iu <= iGuHi; iu++) { if (iu < 0 || iu >= iGrid) continue; const double dDu = fabs (iu - dGu); const double dKu = vecKernel[static_cast(dDu / dHalfWidth * iTableSize + 0.5)]; if (dKu == 0.) continue; for (int iv = iGvLo; iv <= iGvHi; iv++) { if (iv < 0 || iv >= iGrid) continue; const double dDv = fabs (iv - dGv); const double dK = dKu * vecKernel[static_cast(dDv / dHalfWidth * iTableSize + 0.5)]; if (dK == 0.) continue; const size_t iIndex = static_cast(iu) * iGrid + iv; vecGridReal[iIndex] += cValue.real() * dK; vecGridImag[iIndex] += cValue.imag() * dK; } } } } // ---- inverse transform of the gridded spectrum fftw_complex* pcGrid = static_cast (fftw_malloc (sizeof(fftw_complex) * iGrid * iGrid)); const int iHalf = iGrid / 2; for (int iu = 0; iu < iGrid; iu++) { const int iuShift = (iu + iHalf) % iGrid; // centred grid -> transform order for (int iv = 0; iv < iGrid; iv++) { const int ivShift = (iv + iHalf) % iGrid; const size_t iSrc = static_cast(iu) * iGrid + iv; const size_t iDst = static_cast(iuShift) * iGrid + ivShift; pcGrid[iDst][0] = vecGridReal[iSrc]; pcGrid[iDst][1] = vecGridImag[iSrc]; } } fftw_plan planGrid = fftw_plan_dft_2d (iGrid, iGrid, pcGrid, pcGrid, FFTW_BACKWARD, FFTW_ESTIMATE); fftw_execute (planGrid); fftw_destroy_plan (planGrid); // ---- deapodize, scale and crop the centre of the grid // No extra frequency-area factor here: unlike the interpolation path, the // density weights already carry du*dv, so sum(w*F) is itself the integral // over the frequency plane. Multiplying by dDeltaUg^2 as well would count // the area element twice. const double dScale = 1.; // The image's centre pixel, (nx - 1) / 2, goes on the grid's DC sample at // iGrid / 2. As (iGrid - nx) / 2 this floored for an odd nx and put the // image one grid sample -- half an image pixel -- off DC. const int iOffset = iGrid / 2 - (static_cast(nx) - 1) / 2; ImageFileArray v = rIF.getArray(); for (unsigned int ix = 0; ix < nx; ix++) { const int iGx = iOffset + ix; const double dCx = kaiserBesselFT ((iGx - iGrid / 2.) / iGrid, iKernelWidth, dBeta); for (unsigned int iy = 0; iy < ny; iy++) { const int iGy = iOffset + iy; const double dCy = kaiserBesselFT ((iGy - iGrid / 2.) / iGrid, iKernelWidth, dBeta); const int iuShift = (iGx + iHalf) % iGrid; // transform order -> centred image const int ivShift = (iGy + iHalf) % iGrid; const size_t iIndex = static_cast(iuShift) * iGrid + ivShift; const double dDeapod = dCx * dCy; v[ix][iy] = (dDeapod != 0.) ? pcGrid[iIndex][0] * dScale / dDeapod : 0.; } } fftw_free (pcGrid); if (m_geometry == Scanner::GEOMETRY_EQUIANGULAR || m_geometry == Scanner::GEOMETRY_EQUILINEAR) delete pProj; return true; #endif } // --------------------------------------------------------------------------- // Structured history and extensible metadata (6.5.0) // --------------------------------------------------------------------------- void Projections::addHistory (std::string_view operation, const HistoryParams& params, double dCalcTime) { m_label.setLabelType (Array2dFileLabel::L_HISTORY); m_label.setOperation (operation); m_label.setParameters (params); m_label.setCalcTime (dCalcTime); #ifdef VERSION m_label.setVersion (VERSION); #endif std::string strText (operation); if (! params.empty()) strText += ": " + params.toString(); m_label.setLabelString (strText); m_remark = strText; m_calcTime = dCalcTime; } void Projections::setUserAttribute (std::string_view name, std::string_view value) { m_mapUser[std::string (name)] = std::string (value); } void Projections::setUserAttribute (std::string_view name, double dValue) { std::ostringstream os; os << std::setprecision (17) << dValue; m_mapUser[std::string (name)] = os.str(); } bool Projections::getUserAttribute (std::string_view name, std::string& strValue) const { std::map::const_iterator it = m_mapUser.find (name); if (it == m_mapUser.end()) return false; strValue = it->second; return true; } ctsim-7.0.0/libctsim/scanner.cpp0000644000175000017500000005667415253666241015646 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: scanner.cpp ** Purpose: Classes for CT scanner ** Programmer: Kevin Rosenberg ** Date Started: 1984 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #include "ct.h" #include #include const int Scanner::GEOMETRY_INVALID = -1; const int Scanner::GEOMETRY_PARALLEL = 0; const int Scanner::GEOMETRY_EQUIANGULAR = 1; const int Scanner::GEOMETRY_EQUILINEAR = 2; const char* const Scanner::s_aszGeometryName[] = { "parallel", "equiangular", "equilinear", }; const char* const Scanner::s_aszGeometryTitle[] = { "Parallel", "Equiangular", "Equilinear", }; const int Scanner::s_iGeometryCount = sizeof(s_aszGeometryName) / sizeof(const char*); // NAME // DetectorArray Construct a DetectorArray DetectorArray::DetectorArray (const int nDet) : m_vecDetValues (nDet), m_nDet (nDet) { } // NAME // ~DetectorArray Free memory allocated to a detector array DetectorArray::~DetectorArray (void) { } /* NAME * Scanner::Scanner Construct a user specified detector structure * * SYNOPSIS * Scanner (phm, nDet, nView, nSample) * Phantom& phm PHANTOM that we are making detector for * int geomety Geometry of detector * int nDet Number of detector along detector array * int nView Number of rotated views * int nSample Number of rays per detector */ Scanner::Scanner (const Phantom& phm, std::string_view geometryName, int nDet, int nView, int offsetView, int nSample, const double rot_anglen, const double dFocalLengthRatio, const double dCenterDetectorRatio, const double dViewRatio, const double dScanRatio) { m_fail = false; m_idGeometry = convertGeometryNameToID (geometryName); if (m_idGeometry == GEOMETRY_INVALID) { m_fail = true; m_failMessage = "Invalid geometry name "; m_failMessage += geometryName; return; } if (nView < 1 || nDet < 1) { m_fail = true; m_failMessage = "nView & nDet must be greater than 0"; return; } if (nSample < 1) m_nSample = 1; m_nDet = nDet; m_nView = nView; m_iOffsetView = offsetView; // Never assigned before 7.0.0~m8, yet read by collectProjections as the // position in the projection file to store the first view at. Whatever // happened to be in that memory became an array offset: harmless when it // was zero, a wild write when it was not, which is why it surfaced only in // some builds. A scan starts at the first view. m_startView = 0; m_nSample = nSample; m_dFocalLengthRatio = dFocalLengthRatio; m_dCenterDetectorRatio = dCenterDetectorRatio; m_dViewRatio = dViewRatio; m_dScanRatio = dScanRatio; m_dViewDiameter = phm.getDiameterBoundaryCircle() * m_dViewRatio; m_dFocalLength = (m_dViewDiameter / 2) * m_dFocalLengthRatio; m_dCenterDetectorLength = (m_dViewDiameter / 2) * m_dCenterDetectorRatio; m_dSourceDetectorLength = m_dFocalLength + m_dCenterDetectorLength; m_dScanDiameter = m_dViewDiameter * m_dScanRatio; m_dXCenter = phm.xmin() + (phm.xmax() - phm.xmin()) / 2; m_dYCenter = phm.ymin() + (phm.ymax() - phm.ymin()) / 2; m_rotLen = rot_anglen; m_rotInc = m_rotLen / m_nView; if (m_idGeometry == GEOMETRY_PARALLEL) { m_dFanBeamAngle = 0; m_detLen = m_dScanDiameter; m_detStart = -m_detLen / 2; m_detInc = m_detLen / m_nDet; double dDetectorArrayEndOffset = 0; // For even number of detectors, make detInc slightly larger so that center lies // at nDet/2. Also, extend detector array by one detInc so that all of the phantom is scanned if (isEven (m_nDet)) { // Adjust for Even number of detectors m_detInc = m_detLen / (m_nDet - 1); // center detector = (nDet/2) dDetectorArrayEndOffset = m_detInc; } double dHalfDetLen = m_detLen / 2; m_initPos.xs1 = m_dXCenter - dHalfDetLen; m_initPos.ys1 = m_dYCenter + m_dFocalLength; m_initPos.xs2 = m_dXCenter + dHalfDetLen + dDetectorArrayEndOffset; m_initPos.ys2 = m_dYCenter + m_dFocalLength; m_initPos.xd1 = m_dXCenter - dHalfDetLen; m_initPos.yd1 = m_dYCenter - m_dCenterDetectorLength; m_initPos.xd2 = m_dXCenter + dHalfDetLen + dDetectorArrayEndOffset; m_initPos.yd2 = m_dYCenter - m_dCenterDetectorLength; m_initPos.angle = m_iOffsetView * m_rotInc; m_detLen += dDetectorArrayEndOffset; } else if (m_idGeometry == GEOMETRY_EQUILINEAR) { if (m_dScanDiameter / 2 >= m_dFocalLength) { m_fail = true; m_failMessage = "Invalid geometry: Focal length must be larger than scan length"; return; } const double dAngle = asin ((m_dScanDiameter / 2) / m_dFocalLength); const double dHalfDetLen = m_dSourceDetectorLength * tan (dAngle); m_detLen = dHalfDetLen * 2; m_detStart = -dHalfDetLen; m_detInc = m_detLen / m_nDet; double dDetectorArrayEndOffset = 0; if (isEven (m_nDet)) { // Adjust for Even number of detectors m_detInc = m_detLen / (m_nDet - 1); // center detector = (nDet/2) dDetectorArrayEndOffset = m_detInc; m_detLen += dDetectorArrayEndOffset; } m_dFanBeamAngle = dAngle * 2; m_initPos.xs1 = m_dXCenter; m_initPos.ys1 = m_dYCenter + m_dFocalLength; m_initPos.xs2 = m_dXCenter; m_initPos.ys2 = m_dYCenter + m_dFocalLength; m_initPos.xd1 = m_dXCenter - dHalfDetLen; m_initPos.yd1 = m_dYCenter - m_dCenterDetectorLength; m_initPos.xd2 = m_dXCenter + dHalfDetLen + dDetectorArrayEndOffset; m_initPos.yd2 = m_dYCenter - m_dCenterDetectorLength; m_initPos.angle = m_iOffsetView * m_rotInc; } else if (m_idGeometry == GEOMETRY_EQUIANGULAR) { if (m_dScanDiameter / 2 > m_dFocalLength) { m_fail = true; m_failMessage = "Invalid geometry: Focal length must be larger than scan length"; return; } const double dAngle = asin ((m_dScanDiameter / 2) / m_dFocalLength); m_detLen = 2 * dAngle; m_detStart = -dAngle; m_detInc = m_detLen / m_nDet; double dDetectorArrayEndOffset = 0; if (isEven (m_nDet)) { // Adjust for Even number of detectors m_detInc = m_detLen / (m_nDet - 1); // center detector = (nDet/2) dDetectorArrayEndOffset = m_detInc; } // adjust for center-detector length double dA1 = acos ((m_dScanDiameter / 2) / m_dCenterDetectorLength); double dAngularScale = 2 * (HALFPI + dAngle - dA1) / m_detLen; m_dAngularDetLen = dAngularScale * (m_detLen + dDetectorArrayEndOffset); m_dAngularDetIncrement = dAngularScale * m_detInc; m_initPos.dAngularDet = -m_dAngularDetLen / 2; m_dFanBeamAngle = dAngle * 2; m_initPos.angle = m_iOffsetView * m_rotInc; m_initPos.xs1 = m_dXCenter; m_initPos.ys1 = m_dYCenter + m_dFocalLength;; m_initPos.xs2 = m_dXCenter; m_initPos.ys2 = m_dYCenter + m_dFocalLength; m_detLen += dDetectorArrayEndOffset; } // Calculate incrementatal rotation matrix GRFMTX_2D temp; xlat_mtx2 (m_rotmtxIncrement, -m_dXCenter, -m_dYCenter); rot_mtx2 (temp, m_rotInc); mult_mtx2 (m_rotmtxIncrement, temp, m_rotmtxIncrement); xlat_mtx2 (temp, m_dXCenter, m_dYCenter); mult_mtx2 (m_rotmtxIncrement, temp, m_rotmtxIncrement); } Scanner::~Scanner (void) { } const char* Scanner::convertGeometryIDToName (const int geomID) { const char *name = ""; if (geomID >= 0 && geomID < s_iGeometryCount) return (s_aszGeometryName[geomID]); return (name); } const char* Scanner::convertGeometryIDToTitle (const int geomID) { const char *title = ""; if (geomID >= 0 && geomID < s_iGeometryCount) return (s_aszGeometryName[geomID]); return (title); } int Scanner::convertGeometryNameToID (std::string_view name) { int id = GEOMETRY_INVALID; for (int i = 0; i < s_iGeometryCount; i++) { if (equalsIgnoringCase (name, s_aszGeometryName[i])) { id = i; break; } } return (id); } /* NAME * collectProjections Calculate projections for a Phantom * * SYNOPSIS * collectProjections (proj, phm, start_view, nView, bStoreViewPos, trace) * Projectrions& proj Projection storage * Phantom& phm Phantom for which we collect projections * bool bStoreViewPos true then storage proj at normal view position * int trace Trace level */ void Scanner::collectProjections (Projections& proj, const Phantom& phm, const int trace, SGP* pSGP) { collectProjections (proj, phm, m_startView, proj.nView(), m_iOffsetView, true, trace, pSGP); } void Scanner::collectProjections (Projections& proj, const Phantom& phm, const int iStartView, const int iNumViews, const int iOffsetView, bool bStoreAtViewPosition, const int trace, SGP* pSGP) { int iStorageOffset = (bStoreAtViewPosition ? iStartView : 0); collectProjections (proj, phm, iStartView, iNumViews, iOffsetView, iStorageOffset, trace, pSGP); } static void mtx2_offset_rot (GRFMTX_2D m, double angle, double x, double y) { GRFMTX_2D temp; xlat_mtx2 (m, -x, -y); rot_mtx2 (temp, angle); mult_mtx2 (m, temp, m); xlat_mtx2 (temp, x, y); mult_mtx2 (m, temp, m); } void Scanner::collectProjections (Projections& proj, const Phantom& phm, const int iStartView, const int iNumViews, const int iOffsetView, int iStorageOffset, const int trace, SGP* pSGP) { m_trace = trace; double start_angle = (iStartView + iOffsetView) * proj.rotInc(); #ifdef HAVE_OPENMP #pragma omp parallel for #endif for (int iView = 0; iView < iNumViews; iView++) { double viewAngle = start_angle + (iView * proj.rotInc()); // With OpenMP, need to calculate source and detector positions at each view GRFMTX_2D rotmtx; mtx2_offset_rot (rotmtx, viewAngle, m_dXCenter, m_dYCenter); double xd1=0, yd1=0, xd2=0, yd2=0; if (m_idGeometry != GEOMETRY_EQUIANGULAR) { xd1 = m_initPos.xd1; yd1 = m_initPos.yd1; xd2 = m_initPos.xd2; yd2 = m_initPos.yd2; xform_mtx2 (rotmtx, xd1, yd1); // rotate detector endpoints xform_mtx2 (rotmtx, xd2, yd2); // to initial view_angle } double xs1 = m_initPos.xs1, ys1 = m_initPos.ys1; double xs2 = m_initPos.xs2, ys2 = m_initPos.ys2; xform_mtx2 (rotmtx, xs1, ys1); // rotate source endpoints to xform_mtx2 (rotmtx, xs2, ys2); // initial view angle int iStoragePosition = iView + iStorageOffset; DetectorArray& detArray = proj.getDetectorArray( iStoragePosition ); #ifdef HAVE_SGP if (pSGP && m_trace >= Trace::TRACE_PHANTOM) { m_pSGP = pSGP; // The window has to contain everything drawn -- the source circle, the // detector arc, the detector array and the scan circle -- with room at // the top for the four rows of parameters, which are drawn in the same // coordinates. The margin here is what those four rows occupy. static const double dTextBandFraction = 1.42; const double dContentRadius = dmax (dmax (m_dFocalLength, m_dSourceDetectorLength - m_dFocalLength), dmax (m_detLen / 2, m_dViewDiameter / 2)); double dHalfWindowSize = dContentRadius * dTextBandFraction; // The window stays square, so circles stay circles, but it is shifted // up in world terms -- which puts the drawing lower on the screen, with // the empty band above it where the parameters go rather than split // evenly above and below. const double dTextBandShift = dContentRadius * 0.34; m_dXMinWin = m_dXCenter - dHalfWindowSize; m_dXMaxWin = m_dXCenter + dHalfWindowSize; m_dYMinWin = m_dYCenter - dHalfWindowSize + dTextBandShift; m_dYMaxWin = m_dYCenter + dHalfWindowSize + dTextBandShift; m_pSGP->setWindow (m_dXMinWin, m_dYMinWin, m_dXMaxWin, m_dYMaxWin); m_pSGP->setRasterOp (RO_COPY); m_pSGP->setColor (C_RED); m_pSGP->moveAbs (0., 0.); m_pSGP->drawCircle (m_dViewDiameter / 2); m_pSGP->moveAbs (0., 0.); m_pSGP->setColor (C_GREEN); m_pSGP->drawCircle (m_dFocalLength); m_pSGP->setColor (C_BLUE); // The labels are read at whatever size the trace window is showing, so // their size follows the drawing rather than being a fixed 9 points, // which was a few pixels tall in the 900-pixel image the Qt trace // window draws into. const int iTextPoints = std::max (9, std::min (26, m_pSGP->getDriver().getPhysicalYSize() / 46)); m_pSGP->setTextPointSize (iTextPoints); phm.draw (*m_pSGP); m_dTextHeight = m_pSGP->getCharHeight (); traceShowParam ("Phantom:", "%s", PROJECTION_TRACE_ROW_PHANT_ID, C_BLACK, phm.name().c_str()); traceShowParam ("Geometry:", "%s", PROJECTION_TRACE_ROW_GEOMETRY, C_BLUE, convertGeometryIDToName(m_idGeometry)); traceShowParam ("Focal Length Ratio:", "%.2f", PROJECTION_TRACE_ROW_FOCAL_LENGTH, C_BLUE, m_dFocalLengthRatio); // traceShowParam ("Field Of View Ratio:", "%.2f", PROJECTION_TRACE_ROW_FIELD_OF_VIEW, C_BLUE, m_dFieldOfViewRatio); traceShowParam ("Num Detectors:", "%d", PROJECTION_TRACE_ROW_NDET, C_BLUE, proj.nDet()); traceShowParam ("Num Views:", "%d", PROJECTION_TRACE_ROW_NVIEW, C_BLUE, proj.nView()); traceShowParam ("Samples / Ray:", "%d", PROJECTION_TRACE_ROW_SAMPLES, C_BLUE, m_nSample); m_pSGP->setMarker (SGP::MARKER_BDIAMOND); m_pSGP->setColor (C_BLACK); m_pSGP->setPenWidth (2); if (m_idGeometry == GEOMETRY_PARALLEL) { m_pSGP->moveAbs (xs1, ys1); m_pSGP->lineAbs (xs2, ys2); m_pSGP->moveAbs (xd1, yd1); m_pSGP->lineAbs (xd2, yd2); } else if (m_idGeometry == GEOMETRY_EQUILINEAR) { m_pSGP->setPenWidth (4); m_pSGP->moveAbs (xs1, ys1); m_pSGP->lineAbs (xs2, ys2); m_pSGP->setPenWidth (2); m_pSGP->moveAbs (xd1, yd1); m_pSGP->lineAbs (xd2, yd2); } else if (m_idGeometry == GEOMETRY_EQUIANGULAR) { m_pSGP->setPenWidth (4); m_pSGP->moveAbs (xs1, ys1); m_pSGP->lineAbs (xs2, ys2); m_pSGP->setPenWidth (2); m_pSGP->moveAbs (0., 0.); m_pSGP->drawArc (m_dCenterDetectorLength, viewAngle + 3 * HALFPI - (m_dAngularDetLen/2), viewAngle + 3 * HALFPI + (m_dAngularDetLen/2)); } m_pSGP->setPenWidth (1); } if (m_trace > Trace::TRACE_CONSOLE) traceShowParam ("Current View:", "%d (%.0f%%)", PROJECTION_TRACE_ROW_CURR_VIEW, C_RED, iView + iStartView, (iView + iStartView) / static_cast(m_nView) * 100.); #endif if (m_trace == Trace::TRACE_CONSOLE) std::cout << "Current View: " << iView+iStartView << std::endl; projectSingleView (phm, detArray, xd1, yd1, xd2, yd2, xs1, ys1, xs2, ys2, viewAngle + 3 * HALFPI); detArray.setViewAngle (viewAngle); #ifdef HAVE_SGP if (m_pSGP && m_trace >= Trace::TRACE_PHANTOM) { // rs_plot (detArray, xd1, yd1, dXCenter, dYCenter, theta); } #endif } /* for each iView */ } /* NAME * rayview Calculate raysums for a view at any angle * * SYNOPSIS * rayview (phm, detArray, xd1, nSample, yd1, xd2, yd2, xs1, ys1, xs2, ys2) * Phantom& phm Phantom to scan * DETARRAY *detArray Storage of values for detector array * Scanner& det Scanner parameters * double xd1, yd1, xd2, yd2 Beginning & ending detector positions * double xs1, ys1, xs2, ys2 Beginning & ending source positions * * RAY POSITIONING * For each detector, have there are a variable number of rays traced. * The source of each ray is the center of the source x-ray cell. The * detector positions are equally spaced within the cell * * The increments between rays are calculated so that the cells start * at the beginning of a detector cell and they end on the endpoint * of the cell. Thus, the last cell starts at (xd2-ddx),(yd2-ddy). * The exception to this is if there is only one ray per detector. * In that case, the detector position is the center of the detector cell. */ void Scanner::projectSingleView (const Phantom& phm, DetectorArray& detArray, const double xd1, const double yd1, const double xd2, const double yd2, const double xs1, const double ys1, const double xs2, const double ys2, const double dDetAngle) { double sdx = (xs2 - xs1) / detArray.nDet(); // change in coords double sdy = (ys2 - ys1) / detArray.nDet(); // between source double xs_maj = xs1 + (sdx / 2); // put ray source in center of cell double ys_maj = ys1 + (sdy / 2); double ddx=0, ddy=0, ddx2=0, ddy2=0, ddx2_ofs=0, ddy2_ofs=0, xd_maj=0, yd_maj=0; double dAngleInc=0, dAngleSampleInc=0, dAngleSampleOffset=0, dAngleMajor=0; if (m_idGeometry == GEOMETRY_EQUIANGULAR) { dAngleInc = m_dAngularDetIncrement; dAngleSampleInc = dAngleInc / m_nSample; dAngleSampleOffset = dAngleSampleInc / 2; dAngleMajor = dDetAngle - (m_dAngularDetLen/2) + dAngleSampleOffset; } else { ddx = (xd2 - xd1) / detArray.nDet(); // change in coords ddy = (yd2 - yd1) / detArray.nDet(); // between detectors ddx2 = ddx / m_nSample; // Incr. between rays with detector cell ddy2 = ddy / m_nSample; // Doesn't include detector endpoints ddx2_ofs = ddx2 / 2; // offset of 1st ray from start of detector cell ddy2_ofs = ddy2 / 2; xd_maj = xd1 + ddx2_ofs; // Incr. between detector cells yd_maj = yd1 + ddy2_ofs; } const std::span detval = detArray.detValues(); if (phm.getComposition() == P_UNIT_PULSE) { // put unit pulse in center of view for (int d = 0; d < detArray.nDet(); d++) detval[d] = 0; detval[ detArray.nDet() / 2 ] = 1; } else { for (int d = 0; d < detArray.nDet(); d++) { double xs = xs_maj; double ys = ys_maj; double xd=0, yd=0, dAngle=0; if (m_idGeometry == GEOMETRY_EQUIANGULAR) { dAngle = dAngleMajor; } else { xd = xd_maj; yd = yd_maj; } double sum = 0.0; for (unsigned int i = 0; i < m_nSample; i++) { if (m_idGeometry == GEOMETRY_EQUIANGULAR) { xd = m_dCenterDetectorLength * cos (dAngle); yd = m_dCenterDetectorLength * sin (dAngle); } #ifdef HAVE_SGP if (m_pSGP && m_trace >= Trace::TRACE_PROJECTIONS) { m_pSGP->setColor (C_YELLOW); m_pSGP->setRasterOp (RO_AND); m_pSGP->moveAbs (xs, ys); m_pSGP->lineAbs (xd, yd); } #endif sum += projectSingleLine (phm, xd, yd, xs, ys); #ifdef HAVE_SGP // if (m_trace >= Trace::TRACE_CLIPPING) { // traceShowParam ("Attenuation:", "%s", PROJECTION_TRACE_ROW_ATTEN, C_LTMAGENTA, " "); // traceShowParam ("Attenuation:", "%.3f", PROJECTION_TRACE_ROW_ATTEN, C_LTMAGENTA, sum); // } #endif if (m_idGeometry == GEOMETRY_EQUIANGULAR) dAngle += dAngleSampleInc; else { xd += ddx2; yd += ddy2; } } // for each sample in detector detval[d] = sum / m_nSample; xs_maj += sdx; ys_maj += sdy; if (m_idGeometry == GEOMETRY_EQUIANGULAR) dAngleMajor += dAngleInc; else { xd_maj += ddx; yd_maj += ddy; } } /* for each detector */ } /* if not unit pulse */ } void Scanner::traceShowParam (const char *szLabel, const char *fmt, int row, int color, ...) { va_list arg; va_start(arg, color); #ifdef HAVE_SGP traceShowParamRasterOp (RO_COPY, szLabel, fmt, row, color, arg); #else traceShowParamRasterOp (0, szLabel, fmt, row, color, arg); #endif va_end(arg); } void Scanner::traceShowParamXOR (const char *szLabel, const char *fmt, int row, int color, ...) { va_list arg; va_start(arg, color); #ifdef HAVE_SGP traceShowParamRasterOp (RO_XOR, szLabel, fmt, row, color, arg); #else traceShowParamRasterOp (0, szLabel, fmt, row, color, arg); #endif va_end(arg); } void Scanner::traceShowParamRasterOp (int iRasterOp, const char *szLabel, const char *fmt, int row, int color, va_list args) { char szValue[256]; vsnprintf (szValue, sizeof(szValue), fmt, args); #ifdef HAVE_SGP if (m_pSGP) { m_pSGP->setRasterOp (iRasterOp); m_pSGP->setTextColor (color, -1); double dValueOffset = (m_dXMaxWin - m_dXMinWin) / 4; if (row < 4) { double dYPos = m_dYMaxWin - (row * m_dTextHeight); double dXPos = m_dXMinWin; m_pSGP->moveAbs (dXPos, dYPos); m_pSGP->drawText (szLabel); m_pSGP->moveAbs (dXPos + dValueOffset, dYPos); m_pSGP->drawText (szValue); } else { row -= 4; double dYPos = m_dYMaxWin - (row * m_dTextHeight); double dXPos = m_dXMinWin + (m_dXMaxWin - m_dXMinWin) * 0.5; m_pSGP->moveAbs (dXPos, dYPos); m_pSGP->drawText (szLabel); m_pSGP->moveAbs (dXPos + dValueOffset, dYPos); m_pSGP->drawText (szValue); } } else #endif { cio_put_str (szLabel); cio_put_str (szValue); cio_put_str ("\n"); } } /* NAME * projectSingleLine INTERNAL: Calculates raysum along a line for a Phantom * * SYNOPSIS * rsum = phm_ray_attenuation (phm, x1, y1, x2, y2) * double rsum Ray sum of Phantom along given line * Phantom& phm; Phantom from which to calculate raysum * double *x1, *y1, *x2, y2 Endpoints of ray path (in Phantom coords) */ double Scanner::projectSingleLine (const Phantom& phm, const double x1, const double y1, const double x2, const double y2) { // check ray against each pelem in Phantom double rsum = 0.0; for (PElemConstIterator i = phm.listPElem().begin(); i != phm.listPElem().end(); i++) rsum += projectLineAgainstPElem (**i, x1, y1, x2, y2); return (rsum); } /* NAME * pelem_ray_attenuation Calculate raysum of an pelem along one line * * SYNOPSIS * rsum = pelem_ray_attenuation (pelem, x1, y1, x2, y2) * double rsum Computed raysum * PhantomElement& pelem Pelem to scan * double x1, y1, x2, y2 Endpoints of raysum line */ double Scanner::projectLineAgainstPElem (const PhantomElement& pelem, double x1, double y1, double x2, double y2) { if (pelem.isRaster()) // exact integral through the pixel grid return pelem.lineIntegral (x1, y1, x2, y2); if (! pelem.clipLineWorldCoords (x1, y1, x2, y2)) { if (m_trace == Trace::TRACE_CLIPPING) cio_tone (1000., 0.05); return (0.0); } #ifdef HAVE_SGP if (m_pSGP && m_trace == Trace::TRACE_CLIPPING) { m_pSGP->setRasterOp (RO_XOR); m_pSGP->moveAbs (x1, y1); m_pSGP->lineAbs (x2, y2); cio_tone (8000., 0.05); m_pSGP->moveAbs (x1, y1); m_pSGP->lineAbs (x2, y2); m_pSGP->setRasterOp (RO_SET); } #endif double len = lineLength (x1, y1, x2, y2); return (len * pelem.atten()); } ctsim-7.0.0/libctsim/trace.cpp0000644000175000017500000000474415253543770015303 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: trace.cpp Class for trace ** Programmer: Kevin Rosenberg ** Date Started: June 2000 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #include "ct.h" const int Trace::TRACE_INVALID = -1; const int Trace::TRACE_NONE = 0; const int Trace::TRACE_CONSOLE = 1; const int Trace::TRACE_PHANTOM = 2; const int Trace::TRACE_PROJECTIONS = 3; const int Trace::TRACE_PLOT = 4; const int Trace::TRACE_CLIPPING = 5; const int Trace::BIT_CONSOLE = 0x0001; const int Trace::BIT_PHANTOM = 0x0002; const int Trace::BIT_PROJECTIONS = 0x0004; const int Trace::BIT_PLOT = 0x0008; const int Trace::BIT_CLIPPING = 0x0010; const char* Trace::s_aszTraceName[] = { "none", "console", "phantom", "proj", "plot", "clipping", }; const char* Trace::s_aszTraceTitle[] = { "None", "Console", "Phantom", "Projections", "Plot", "Clipping", }; const int Trace::s_iTraceCount = sizeof(s_aszTraceName) / sizeof(const char*); std::string_view Trace::convertTraceIDToName (const int idTrace) { const char *name = ""; if (idTrace >= 0 && idTrace < s_iTraceCount) return (s_aszTraceName[idTrace]); return (name); } const char* Trace::convertTraceIDToTitle (const int idTrace) { const char *title = ""; if (idTrace >= 0 && idTrace < s_iTraceCount) return (s_aszTraceName[idTrace]); return (title); } int Trace::convertTraceNameToID (std::string_view name) { int id = Trace::TRACE_INVALID; for (int i = 0; i < s_iTraceCount; i++) if (equalsIgnoringCase (name, s_aszTraceName[i])) { id = i; break; } return (id); } ctsim-7.0.0/libctsim/filter.cpp0000644000175000017500000004225415253666241015467 0ustar kevinkevin/***************************************************************************** ** File IDENTIFICATION ** ** Name: filter.cpp ** Purpose: Routines for signal-procesing filters ** Progammer: Kevin Rosenberg ** Date Started: Aug 1984 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #include "ct.h" #include #include int SignalFilter::N_INTEGRAL=500; //static member const int SignalFilter::FILTER_INVALID = -1 ; const int SignalFilter::FILTER_ABS_BANDLIMIT = 0; // filter times |x| const int SignalFilter::FILTER_ABS_G_HAMMING = 1; const int SignalFilter::FILTER_ABS_HANNING = 2; const int SignalFilter::FILTER_ABS_COSINE = 3; const int SignalFilter::FILTER_ABS_SINC = 4; const int SignalFilter::FILTER_BANDLIMIT = 5; const int SignalFilter::FILTER_SINC = 6; const int SignalFilter::FILTER_G_HAMMING = 7; const int SignalFilter::FILTER_HANNING = 8; const int SignalFilter::FILTER_COSINE = 9; const int SignalFilter::FILTER_TRIANGLE = 10; const int SignalFilter::s_iReconstructFilterCount = 4; const char* const SignalFilter::s_aszFilterName[] = { "abs_bandlimit", "abs_hamming", "abs_hanning", "abs_cosine", "abs_sinc", "bandlimit", "sinc", "hamming", "hanning", "cosine", "triangle" }; const char* const SignalFilter::s_aszFilterTitle[] = { "Abs(w) * Bandlimit", "Abs(w) * Hamming", "Abs(w) * Hanning", "Abs(w) * Cosine", "Abs(w) * Sinc", "Bandlimit", "Sinc", "Hamming", "Hanning", "Cosine", "Triangle" }; const int SignalFilter::s_iFilterCount = sizeof(s_aszFilterName) / sizeof(const char*); const int SignalFilter::DOMAIN_INVALID = -1; const int SignalFilter::DOMAIN_FREQUENCY = 0; const int SignalFilter::DOMAIN_SPATIAL = 1; const char* const SignalFilter::s_aszDomainName[] = { "frequency", "spatial", }; const char* const SignalFilter::s_aszDomainTitle[] = { "Frequency", "Spatial", }; const int SignalFilter::s_iDomainCount = sizeof(s_aszDomainName) / sizeof(const char*); /* NAME * SignalFilter::SignalFilter Construct a signal * * SYNOPSIS * f = SignalFilter (filt_type, bw, filterMin, filterMax, n, param, domain, analytic) * double f Generated filter vector * int filt_type Type of filter wanted * double bw Bandwidth of filter * double filterMin, filterMax Filter limits * int nFilterPoints Number of points in signal * double param General input parameter to filters * int domain FREQUENCY or SPATIAL domain wanted */ SignalFilter::SignalFilter (const char* szFilterName, double dFilterMinimum, double dFilterMaximum, int nFilterPoints, double dBandwidth, double dFilterParam, const char* szDomainName) : m_fail (false) { m_idFilter = convertFilterNameToID (szFilterName); if (m_idFilter == FILTER_INVALID) { m_fail = true; m_failMessage = "Invalid Filter name "; m_failMessage += szFilterName; return; } m_idDomain = convertDomainNameToID (szDomainName); if (m_idDomain == DOMAIN_INVALID) { m_fail = true; m_failMessage = "Invalid domain name "; m_failMessage += szDomainName; return; } init (m_idFilter, dFilterMinimum, dFilterMaximum, nFilterPoints, dBandwidth, dFilterParam, m_idDomain); } SignalFilter::SignalFilter (const int idFilter, double dFilterMinimum, double dFilterMaximum, int nFilterPoints, double dBandwidth, double dFilterParam, const int idDomain) : m_fail (false) { init (idFilter, dFilterMinimum, dFilterMaximum, nFilterPoints, dBandwidth, dFilterParam, idDomain); } SignalFilter::SignalFilter (const char* szFilterName, const char* szDomainName, double dBandwidth, double dFilterParam) : m_fail (false) { m_nFilterPoints = 0; m_dBandwidth = dBandwidth; m_dFilterParam = dFilterParam; m_idFilter = convertFilterNameToID (szFilterName); if (m_idFilter == FILTER_INVALID) { m_fail = true; m_failMessage = "Invalid Filter name "; m_failMessage += szFilterName; return; } m_idDomain = convertDomainNameToID (szDomainName); if (m_idDomain == DOMAIN_INVALID) { m_fail = true; m_failMessage = "Invalid domain name "; m_failMessage += szDomainName; return; } } void SignalFilter::init (const int idFilter, double dFilterMinimum, double dFilterMaximum, int nFilterPoints, double dBandwidth, double dFilterParam, const int idDomain) { m_idFilter = idFilter; m_idDomain = idDomain; if (m_idFilter == FILTER_INVALID || m_idDomain == DOMAIN_INVALID) { m_fail = true; return; } if (nFilterPoints < 2) { m_fail = true; // += on an int appends the character with that code, not the number, // and the third line assigned rather than appended, discarding the // first two. The message read " less than 2" with no count in it. m_failMessage = "Number of filter points " + std::to_string (nFilterPoints) + " less than 2"; return; } m_nameFilter = convertFilterIDToName (m_idFilter); m_nameDomain = convertDomainIDToName (m_idDomain); m_nFilterPoints = nFilterPoints; m_dFilterParam = dFilterParam; m_dBandwidth = dBandwidth; m_dFilterMin = dFilterMinimum; m_dFilterMax = dFilterMaximum; m_dFilterInc = (m_dFilterMax - m_dFilterMin) / (m_nFilterPoints - 1); m_vecFilter.assign (m_nFilterPoints, 0.); if (m_idDomain == DOMAIN_FREQUENCY) createFrequencyFilter (m_vecFilter.data()); else if (m_idDomain == DOMAIN_SPATIAL) createSpatialFilter (m_vecFilter.data()); } SignalFilter::~SignalFilter (void) { } void SignalFilter::createFrequencyFilter (double* adFilter) const { double xstart = m_dFilterMin; #if HAVE_OPENMP #pragma omp parallel for #endif for (int i = 0; i < m_nFilterPoints; i++) { double x = xstart + (i * m_dFilterInc); adFilter[i] = frequencyResponse (x); } } void SignalFilter::createSpatialFilter (double* adFilter) const { if (m_idFilter == FILTER_ABS_SINC) { // The closed form of the inverse transform of |w| sinc(pi w / bw): // with the sample spacing tau = 1 / bw and x = n tau, // h[0] = 2 bw^2 / pi^2, h[n] = -2 bw^2 / (pi^2 (4 n^2 - 1)) // The kernel sums to zero, which is what makes it high-pass. It was // written as 1 / bw^2 -- the bandwidth the wrong way up and without // the 2 / pi^2 -- so every tap was wrong by a factor that grew with // the bandwidth. const double dScale = 2. * m_dBandwidth * m_dBandwidth / (PI * PI); const int center = (m_nFilterPoints - 1) / 2; adFilter[center] = dScale; for (int i = 1; i <= center; i++) adFilter[center + i] = adFilter[center - i] = -dScale / (4. * i * i - 1.); } else { double xstart = m_dFilterMin; #if HAVE_OPENMP #pragma omp parallel for #endif for (int i = 0; i < m_nFilterPoints; i++) { double x = xstart + (i * m_dFilterInc); if (haveAnalyticSpatial(m_idFilter)) adFilter[i] = spatialResponseAnalytic (x); else adFilter[i] = spatialResponseCalc (x); } } } int SignalFilter::convertFilterNameToID (std::string_view name) { int filterID = FILTER_INVALID; for (int i = 0; i < s_iFilterCount; i++) { if (equalsIgnoringCase (name, s_aszFilterName[i])) { filterID = i; break; } } return (filterID); } const char * SignalFilter::convertFilterIDToName (const int filterID) { static const char *name = ""; if (filterID >= 0 && filterID < s_iFilterCount) return (s_aszFilterName [filterID]); return (name); } const char * SignalFilter::convertFilterIDToTitle (const int filterID) { static const char *title = ""; if (filterID >= 0 && filterID < s_iFilterCount) { return (s_aszFilterTitle [filterID]); } return (title); } int SignalFilter::convertDomainNameToID (std::string_view name) { int dID = DOMAIN_INVALID; for (int i = 0; i < s_iDomainCount; i++) { if (equalsIgnoringCase (name, s_aszDomainName[i])) { dID = i; break; } } return (dID); } const char * SignalFilter::convertDomainIDToName (const int domainID) { static const char *name = ""; if (domainID >= 0 && domainID < s_iDomainCount) return (s_aszDomainName [domainID]); return (name); } const char * SignalFilter::convertDomainIDToTitle (const int domainID) { static const char *title = ""; if (domainID >= 0 && domainID < s_iDomainCount) return (s_aszDomainTitle [domainID]); return (title); } double SignalFilter::response (double x) { double response = 0; if (m_idDomain == DOMAIN_SPATIAL) response = spatialResponse (m_idFilter, m_dBandwidth, x, m_dFilterParam); else if (m_idDomain == DOMAIN_FREQUENCY) response = frequencyResponse (m_idFilter, m_dBandwidth, x, m_dFilterParam); return (response); } double SignalFilter::spatialResponse (int filterID, double bw, double x, double param) { if (haveAnalyticSpatial(filterID)) return spatialResponseAnalytic (filterID, bw, x, param); else return spatialResponseCalc (filterID, bw, x, param, N_INTEGRAL); } void SignalFilter::copyFilterData (double* pdFilter, const int iStart, const int nPoints) const { int iFirst = clamp (iStart, 0, m_nFilterPoints - 1); int iLast = clamp (iFirst + nPoints - 1, 0, m_nFilterPoints - 1); for (int i = iFirst; i <= iLast; i++) pdFilter[i - iFirst] = m_vecFilter[i]; } /* NAME * filter_spatial_response_calc Calculate filter by discrete inverse fourier * transform of filters's frequency * response * * SYNOPSIS * y = filter_spatial_response_calc (filt_type, x, m_bw, param, n) * double y Filter's response in spatial domain * int filt_type Type of filter (definitions in ct.h) * double x Spatial position to evaluate filter * double m_bw Bandwidth of window * double param General parameter for various filters * int n Number of points to calculate integrations */ double SignalFilter::spatialResponseCalc (double x) const { return (spatialResponseCalc (m_idFilter, m_dBandwidth, x, m_dFilterParam, N_INTEGRAL)); } double SignalFilter::spatialResponseCalc (int filterID, double bw, double x, double param, int n) { double zmin, zmax; if (filterID == FILTER_TRIANGLE) { zmin = 0; zmax = bw; } else { zmin = 0; zmax = bw / 2; } double zinc = (zmax - zmin) / (n - 1); std::vector q (n); // Deliberately serial: the caller (createSpatialFilter) is already an OpenMP // parallel loop, so a pragma here would only nest. Increment z rather than // multiplying it out. double z = zmin; for (int i = 0; i < n; i++, z += zinc) q[i] = frequencyResponse (filterID, bw, z, param) * cos (TWOPI * z * x); double y = 2 * integrateSimpson (zmin, zmax, q.data(), n); return (y); } /* NAME * filter_frequency_response Return filter frequency response * * SYNOPSIS * h = filter_frequency_response (filt_type, u, m_bw, param) * double h Filters frequency response at u * int filt_type Type of filter * double u Frequency to evaluate filter at * double m_bw Bandwidth of filter * double param General input parameter for various filters */ double SignalFilter::frequencyResponse (double u) const { return frequencyResponse (m_idFilter, m_dBandwidth, u, m_dFilterParam); } double SignalFilter::frequencyResponse (int filterID, double bw, double u, double param) { double q; double au = fabs (u); double abw = fabs (bw); switch (filterID) { case FILTER_BANDLIMIT: if (au >= (abw / 2) + F_EPSILON) q = 0.; else q = 1; break; case FILTER_ABS_BANDLIMIT: if (au >= (abw / 2) + F_EPSILON) q = 0.; else q = au; break; case FILTER_TRIANGLE: if (au >= (abw / 2) + F_EPSILON) q = 0; else q = 1 - au / abw; break; case FILTER_COSINE: if (au >= (abw / 2) + F_EPSILON) q = 0; else q = cos(PI * au / abw); break; case FILTER_ABS_COSINE: if (au >= (abw / 2) + F_EPSILON) q = 0; else q = au * cos(PI * au / abw); break; case FILTER_SINC: q = abw * sinc (PI * abw * au, 1.); break; case FILTER_ABS_SINC: // |w| sinc(pi w / bw) over |w| <= bw/2: the ramp multiplied by a sinc // window, which rolls off gently where a plain band-limit cuts off // abruptly and rings. The sinc's argument is w as a fraction of the // band edge, so it reaches pi/2 there whatever the bandwidth. if (au >= (abw / 2) + F_EPSILON) q = 0; else q = au * sinc (PI * au / abw, 1.); break; case FILTER_HANNING: param = 0.5; [[fallthrough]]; // to G_HAMMING case FILTER_G_HAMMING: if (au >= (abw / 2) + F_EPSILON) q = 0; else q = param + (1 - param) * cos (TWOPI * au / abw); break; case FILTER_ABS_HANNING: param = 0.5; [[fallthrough]]; // to ABS_G_HAMMING case FILTER_ABS_G_HAMMING: if (au >= (abw / 2) + F_EPSILON) q = 0; else q = au * (param + (1 - param) * cos(TWOPI * au / abw)); break; default: q = 0; sys_error (ERR_WARNING, "Frequency response for filter {} not implemented [filter_frequency_response]", filterID); break; } return (q); } /* NAME * filter_spatial_response_analytic Calculate filter by analytic inverse fourier * transform of filters's frequency * response * * SYNOPSIS * y = filter_spatial_response_analytic (filt_type, x, m_bw, param) * double y Filter's response in spatial domain * int filt_type Type of filter (definitions in ct.h) * double x Spatial position to evaluate filter * double m_bw Bandwidth of window * double param General parameter for various filters */ double SignalFilter::spatialResponseAnalytic (double x) const { return spatialResponseAnalytic (m_idFilter, m_dBandwidth, x, m_dFilterParam); } bool SignalFilter::haveAnalyticSpatial (int filterID) { bool haveAnalytic = false; switch (filterID) { case FILTER_BANDLIMIT: case FILTER_TRIANGLE: case FILTER_COSINE: case FILTER_G_HAMMING: case FILTER_HANNING: case FILTER_ABS_BANDLIMIT: case FILTER_ABS_COSINE: case FILTER_ABS_G_HAMMING: case FILTER_ABS_HANNING: case FILTER_SINC: haveAnalytic = true; break; default: break; } return (haveAnalytic); } double SignalFilter::spatialResponseAnalytic (int filterID, double bw, double x, double param) { double q, temp; double u = TWOPI * x; double w = bw / 2; double b = PI / bw; double b2 = TWOPI / bw; switch (filterID) { case FILTER_BANDLIMIT: q = bw * sinc(u * w, 1.0); break; case FILTER_TRIANGLE: temp = sinc (u * w, 1.0); q = bw * temp * temp; break; case FILTER_COSINE: q = sinc(b-u,w) + sinc(b+u,w); break; case FILTER_HANNING: param = 0.5; [[fallthrough]]; // to G_HAMMING case FILTER_G_HAMMING: // sin(u*w)/u is 0/0 at the centre, where x and so u are zero; its // limit there is w. Without this the middle pixel of a spatial // Hamming or Hanning came out NaN, which then spoiled the plot of // any column through it. q = 2 * param * (fabs (u) < F_EPSILON ? w : sin (u * w) / u) + (1 - param) * (sinc (b2 - u, w) + sinc (b2 + u, w)); break; case FILTER_ABS_BANDLIMIT: q = 2 * integral_abscos (u, w); break; case FILTER_ABS_COSINE: q = integral_abscos(b-u,w) + integral_abscos(b+u,w); break; case FILTER_ABS_HANNING: param = 0.5; [[fallthrough]]; // to ABS_G_HAMMING case FILTER_ABS_G_HAMMING: q = 2 * param * integral_abscos(u,w) + (1-param)*(integral_abscos(u-b2,w)+integral_abscos(u+b2,w)); break; case FILTER_SINC: if (fabs (x) < bw / 2) q = 1.; else q = 0.; break; case FILTER_ABS_SINC: default: sys_error (ERR_WARNING, "Analytic filter type {} not implemented [filter_spatial_response_analytic]", filterID); q = 0; break; } return (q); } // Functions that are inline in filter.h // sinc Return sin(x)/x function // v = sinc (x, mult) // Calculates sin(x * mult) / x; // integral_abscos Returns integral of u*cos(u) // // q = integral_abscos (u, w) // double q Integral value // double u Integration variable // double w Upper integration boundary // Returns the value of integral of u*cos(u)*dV for V = 0 to w ctsim-7.0.0/README.md0000644000175000017500000001121715253543770013143 0ustar kevinkevin# CTSim CTSim simulates the process of collecting tomographic X-ray data of phantom objects. These X-ray data through the objects are called projections. CTSim reconstructs the original phantom image from the projections using a variety of algorithms. CTSim is open-source and is covered by the GNU General Public License, version 3 or later (GPL-3.0-or-later). Please enjoy CTSim. I'd like to hear any feedback. Kevin M. Rosenberg, M.D. The official home for CTSim is . ## Brief History CTSim development began in 1983 while I was in medical school. It was written using Lattice C and MS-DOS. I used 8086 assembly language to code convolution and backprojection routines and also to write graphics directly to an IBM CGA video adapter. In 1999, CTSim was ported to GNU/Linux. In 2000, the source code was converted to C++ and published as open source. Functionality was extended through 2018. In 2026, generative AI was used to assist adding test suite, migration to modern C++, converting from the wxWidgets cross-platform GUI to Qt, support HDF5 data files, and other improvements. See the `NEWS` and `ChangeLog` files for more details. ## The programs ### ctsim The graphical application. Phantoms, scans, reconstructions, plots and image operations, with trace modes that draw a scan or a reconstruction as it happens, a 3-D surface view, and background jobs. Built with Qt 6 since 7.0.0; the wxWidgets interface it replaces was removed in that release. `ctsim` also runs the command-line tools, without opening a window: ctsim phm2if head.if 255 255 --phantom herman runs just the phantom-object-to-image-file tool and exits, and `ctsim --shell` reads commands from a prompt, as **File - New Shell** does inside the interface. `ctsim help` lists all commands. ### ctsim-nogui The same program built without the graphical interface, for a machine with no display or no Qt. It offers the same tools and the same prompt. Each tool also works through a symbolic link of its own name, so `phm2if ...` does what `ctsim phm2if ...` does. Until 7.0.0 this program was called `ctsimtext`. ### The individual tools | Tool | What it does | |------|--------------| | `phm2if` | Generates an image file of a phantom object | | `phm2pj` | Simulates the collection of CT data, or projections, of a phantom object | | `pjrec` | Performs a CT reconstruction — image reconstruction from projections. Reads a projection file and writes an image file | | `pjfourier` | Reconstructs by direct Fourier inversion, rather than by backprojection | | `pj2if` | Converts projection data to a raw sinogram image | | `pjinfo` | Shows information on a projection file | | `ifexport` | Converts an image file to a variety of 8-bit and 16-bit image formats | | `if1` | Performs operations on a single image file | | `if2` | Performs operations on two image files, such as comparisons | | `ifinfo` | Shows statistics and history labels of image files | | `png2phm` | Makes a phantom file from a PNG image, the image becoming one raster element | | `ctdump` | Shows what any CTSim file contains, whatever its format | | `ctconvert` | Converts files between HDF5 and the legacy binary format | | `dicomimport` | Imports a DICOM file into CTSim's own format | | `phm2helix`, `pjHinterp` | Helical scanning and the interpolation of helical projections | | `linogram` | Prints linogram sampling | ## Typical usage ### Graphical interface Run `ctsim`. ### Command line When evaluating CT simulation, in general, these steps are followed. Create a phantom image and a viewable image file: phm2if ... ifexport ... Simulate CT data collection and create a viewable image of the raw projections: phm2pj ... pj2if ... ifexport ... Perform the CT reconstruction and create a viewable image file: pjrec ... ifexport ... Display image information and comparative statistics: ifinfo ... There is a sample shell script installed called `sample-ctsim.sh` that performs the above commands. ### Scripts `ctsim --script file` runs a list of commands that make phantoms, scan and reconstruct them, measure the results and assert things about them, all in the graphical interface. **File - Run Script** lists the demonstrations CTSim finds; they are installed under `share/ctsim/demos`. `--workdir` names the directory a run writes into, and `--script-save` fills it with every window the run made, a copy of the script, and a record of what was done. ## Copyright This program is written by Kevin M. Rosenberg, M.D. It is covered by the GNU General Public License (GPL), which allows copying and modifying this code with restrictions. See the file `COPYING` for complete details. ctsim-7.0.0/CMakeLists.txt0000644000175000017500000016437015253765647014446 0ustar kevinkevincmake_minimum_required(VERSION 3.22) project(CTSim VERSION 7.0.0 LANGUAGES C CXX) # CMake 4 normalizes a path before matching it against the exclude patterns # in file(GET_RUNTIME_DEPENDENCIES), which the Windows install rules below # rely on. Without the policy set it warns once for every library it # examines, and it examines every one Windows supplies. Guarded, because # CMake 3 does not know the policy and rejects a name it has never heard of. if(POLICY CMP0207) cmake_policy(SET CMP0207 NEW) endif() # --------------------------------------------------------------------------- # How to install a missing package, in the words of this platform. Naming # apt and brew on Windows told a reader to run commands that do not exist # there; ctsim_install_hint(VAR apt-name brew-name pacman-name) sets VAR to # whichever applies. # --------------------------------------------------------------------------- function(ctsim_install_hint outvar apt brew pacman) if(WIN32) if(pacman STREQUAL "") set(${outvar} "" PARENT_SCOPE) else() set(${outvar} "pacman -S mingw-w64-ucrt-x86_64-${pacman}" PARENT_SCOPE) endif() elseif(APPLE) set(${outvar} "brew install ${brew}" PARENT_SCOPE) else() set(${outvar} "apt install ${apt}" PARENT_SCOPE) endif() endfunction() # Pre-release suffix in Debian's tilde form: "7.0.0~m0" sorts before "7.0.0" # and after every 6.x, which is what we want for the milestone builds of the # Qt port. CMake's project(VERSION) accepts only numbers, so the suffix is # carried separately and joined into VERSION for config.h and the packages. # Empty it for the 7.0.0 release. set(CTSIM_VERSION_SUFFIX "") set(CTSIM_VERSION_FULL "${PROJECT_VERSION}${CTSIM_VERSION_SUFFIX}") string(TOLOWER ${PROJECT_NAME} PROJECT_NAME_LOWER) # --------------------------------------------------------------------------- # Options # --------------------------------------------------------------------------- set(CTSIM_GUI "qt" CACHE STRING "Graphical interface to build: qt, none") set_property(CACHE CTSIM_GUI PROPERTY STRINGS qt none) option(CTSIM_USE_OPENMP "Use OpenMP parallelism in libctsim" ON) option(CTSIM_USE_READLINE "Line editing in the ctsim shell" ON) # Which line-editing library: libedit is preferred because it is BSD licensed # and is what Debian and macOS ship, but GNU readline works equally well now # that CTSim is GPL-3-or-later (see the note in the ChangeLog). set(CTSIM_READLINE "auto" CACHE STRING "Line editing library: auto, editline, readline") set_property(CACHE CTSIM_READLINE PROPERTY STRINGS auto editline readline) option(CTSIM_WITH_HDF5 "HDF5 file format (default for newly written files)" ON) option(CTSIM_BUILD_TESTS "Build regression tests (ctest)" ON) option(CTSIM_WARNINGS "Enable -Wall style warnings" ON) option(CTSIM_NATIVE_ARCH "Tune for the build machine CPU (non-portable)" OFF) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) endif() list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") include(GNUInstallDirs) # GNUInstallDirs derives DOCDIR from the project name, which is "CTSim"; # Debian, the Help menu and every convention want share/doc/ctsim. set(CMAKE_INSTALL_DOCDIR "${CMAKE_INSTALL_DATAROOTDIR}/doc/ctsim") include(CheckIncludeFile) include(CheckFunctionExists) include(TestBigEndian) # --------------------------------------------------------------------------- # Mandatory dependencies: zlib, libpng, FFTW3 (double precision) # --------------------------------------------------------------------------- find_package(ZLIB REQUIRED) find_package(PNG REQUIRED) find_package(PkgConfig) if(PkgConfig_FOUND) pkg_check_modules(FFTW3 IMPORTED_TARGET fftw3) endif() if(NOT FFTW3_FOUND) # e.g. Windows/vcpkg or macOS without pkg-config find_library(FFTW3_LIBRARY NAMES fftw3 libfftw3 fftw3-3) find_path(FFTW3_INCLUDE_DIR fftw3.h) if(FFTW3_LIBRARY AND FFTW3_INCLUDE_DIR) add_library(PkgConfig::FFTW3 UNKNOWN IMPORTED) set_target_properties(PkgConfig::FFTW3 PROPERTIES IMPORTED_LOCATION "${FFTW3_LIBRARY}" INTERFACE_INCLUDE_DIRECTORIES "${FFTW3_INCLUDE_DIR}") set(FFTW3_FOUND TRUE) endif() endif() if(NOT FFTW3_FOUND) ctsim_install_hint(_fftw_hint libfftw3-dev fftw fftw) message(FATAL_ERROR "FFTW3 (double precision) is required: ${_fftw_hint}") endif() # --------------------------------------------------------------------------- # Optional dependencies # --------------------------------------------------------------------------- if(CTSIM_USE_OPENMP) # Apple's clang supports OpenMP but ships neither the runtime nor the # headers, so a plain find_package fails on macOS however the build is # configured. Homebrew's "libomp" keg supplies both; point CMake at it # before asking, which is what the Homebrew instructions in INSTALL assume. if(APPLE AND NOT DEFINED OpenMP_CXX_FLAGS) find_program(_brew_executable brew) if(_brew_executable) execute_process(COMMAND ${_brew_executable} --prefix libomp OUTPUT_VARIABLE _libomp_prefix OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET RESULT_VARIABLE _libomp_result) if(_libomp_result EQUAL 0 AND EXISTS "${_libomp_prefix}/include/omp.h") set(OpenMP_CXX_FLAGS "-Xpreprocessor -fopenmp -I${_libomp_prefix}/include") set(OpenMP_CXX_LIB_NAMES omp) set(OpenMP_omp_LIBRARY "${_libomp_prefix}/lib/libomp.dylib") message(STATUS "OpenMP: using Homebrew libomp at ${_libomp_prefix}") endif() endif() endif() find_package(OpenMP COMPONENTS CXX) set(HAVE_OPENMP ${OpenMP_CXX_FOUND}) if(APPLE AND NOT HAVE_OPENMP) message(STATUS "OpenMP not found: reconstruction and projection run on one " "core. On macOS, \"brew install libomp\" provides it.") endif() endif() if(CTSIM_USE_READLINE) # libedit first. It provides the same readline() / add_history() entry points # through , is BSD licensed, ships with macOS, and is what # Debian already links CTSim against -- so preferring it removes the need for a # distribution patch. if(CTSIM_READLINE STREQUAL "auto" OR CTSIM_READLINE STREQUAL "editline") find_path(EDITLINE_INCLUDE_DIR editline/readline.h) find_library(EDITLINE_LIBRARY NAMES edit) if(EDITLINE_INCLUDE_DIR AND EDITLINE_LIBRARY) set(HAVE_READLINE TRUE) set(HAVE_EDITLINE_READLINE_H TRUE) set(CTSIM_LINEEDIT_INCLUDE_DIR ${EDITLINE_INCLUDE_DIR}) set(CTSIM_LINEEDIT_LIBRARY ${EDITLINE_LIBRARY}) set(CTSIM_LINEEDIT_NAME "libedit") endif() endif() if(NOT HAVE_READLINE AND (CTSIM_READLINE STREQUAL "auto" OR CTSIM_READLINE STREQUAL "readline")) find_path(READLINE_INCLUDE_DIR readline/readline.h) find_library(READLINE_LIBRARY NAMES readline) if(READLINE_INCLUDE_DIR AND READLINE_LIBRARY) set(HAVE_READLINE TRUE) set(HAVE_READLINE_READLINE_H TRUE) set(CTSIM_LINEEDIT_INCLUDE_DIR ${READLINE_INCLUDE_DIR}) set(CTSIM_LINEEDIT_LIBRARY ${READLINE_LIBRARY}) set(CTSIM_LINEEDIT_NAME "GNU readline") endif() endif() if(HAVE_READLINE) # stifle_history caps the saved history. GNU readline has it; the # libedit MSYS2 ships does not, and there the history is simply not # capped. include(CMakePushCheckState) cmake_push_check_state() set(CMAKE_REQUIRED_INCLUDES ${CTSIM_LINEEDIT_INCLUDE_DIR}) set(CMAKE_REQUIRED_LIBRARIES ${CTSIM_LINEEDIT_LIBRARY}) check_function_exists(stifle_history HAVE_STIFLE_HISTORY) cmake_pop_check_state() message(STATUS "shell line editing: ${CTSIM_LINEEDIT_NAME}") else() message(STATUS "shell line editing: none found") endif() endif() if(CTSIM_WITH_HDF5) # 6.5.0: HDF5 is the default format for newly written image and projection # files. Reading detects the format from the file, so legacy files keep # working; a build without HDF5 simply keeps writing the legacy format. find_package(HDF5 COMPONENTS C) if(HDF5_FOUND) set(HAVE_HDF5 TRUE) message(STATUS "HDF5 ${HDF5_VERSION}: default format for new files") else() message(WARNING "HDF5 not found; CTSim will read and write only its legacy format") endif() endif() # DICOM is not optional. It was, when the alternative was a library nobody # packaged; DCMTK is now in every distribution CTSim is built on, and reading # a scanner's own files is too much of the point of the program to be a # build-time choice that half the builds turn off. # # DCMTK >= 3.6.5 ships DCMTKConfig.cmake (Debian/Ubuntu: libdcmtk-dev). ctsim_install_hint(_dcmtk_hint libdcmtk-dev dcmtk dcmtk) find_package(DCMTK QUIET) if(NOT DCMTK_FOUND) message(FATAL_ERROR "DCMTK is required for DICOM support: ${_dcmtk_hint}") endif() set(HAVE_DCMTK TRUE) # Debian/Ubuntu's DCMTKTargets.cmake lists "nsl" in DCMTK::ofstd's # INTERFACE_LINK_LIBRARIES, so every consumer is asked to link -lnsl even # though libnsl is only needed by DCMTK's network module (dcmnet), which CTSim # does not use. None of dcmdata, ofstd or oflog reference it and the linked # binary ends up with no libnsl dependency, so drop it when the library is # absent instead of making people install libnsl-dev for nothing. find_library(CTSIM_NSL_LIBRARY nsl) mark_as_advanced(CTSIM_NSL_LIBRARY) if(NOT CTSIM_NSL_LIBRARY) foreach(_dcmtk_target DCMTK::ofstd DCMTK::oflog DCMTK::dcmdata) if(TARGET ${_dcmtk_target}) get_target_property(_dcmtk_libs ${_dcmtk_target} INTERFACE_LINK_LIBRARIES) if(_dcmtk_libs) list(REMOVE_ITEM _dcmtk_libs nsl) set_target_properties(${_dcmtk_target} PROPERTIES INTERFACE_LINK_LIBRARIES "${_dcmtk_libs}") endif() endif() endforeach() message(STATUS "DCMTK: libnsl not installed; removed from the link interface (unused here)") endif() message(STATUS "DICOM support via DCMTK ${DCMTK_VERSION}") if(NOT CTSIM_GUI STREQUAL "qt" AND NOT CTSIM_GUI STREQUAL "none") message(FATAL_ERROR "CTSIM_GUI=${CTSIM_GUI}: the choices are qt and none. " "\"wx\" and \"both\" went with the wxWidgets interface in 7.0.0; a build " "directory configured before then still has the old value in its cache -- " "reconfigure with -DCTSIM_GUI=qt.") endif() if(CTSIM_GUI STREQUAL "qt") # 6.4 is what Ubuntu 24.04 ships and is enough for the skeleton; later # milestones (QOpenGLWidget) may raise this to 6.5. # Svg ships separately from the base modules on Debian/Ubuntu (qt6-svg-dev), # which is the component most often missing; say so instead of leaving the # generic "Qt6_FOUND is FALSE" message to explain itself. # The 3-D view calls the fixed-function OpenGL API directly, so the GL # library has to be linked by name. Qt supplies it as a side effect on # Linux and macOS, which is why this was never needed until Windows, where # the same calls came back as undefined references to __imp_glLoadIdentity # and its neighbours. OpenGL::GL is opengl32 there, libGL on Linux and the # framework on macOS. set(OpenGL_GL_PREFERENCE GLVND) find_package(OpenGL REQUIRED) find_package(Qt6 6.4 QUIET COMPONENTS Core Gui Widgets OpenGLWidgets PrintSupport Svg Test) if(NOT Qt6_FOUND) set(_qt_missing "") foreach(_mod Core Gui Widgets OpenGLWidgets PrintSupport Svg) if(NOT TARGET Qt6::${_mod}) list(APPEND _qt_missing ${_mod}) endif() endforeach() message(FATAL_ERROR "Qt 6 (>= 6.4) with these components is required for CTSIM_GUI=${CTSIM_GUI}: " "Core Gui Widgets OpenGLWidgets PrintSupport Svg. Missing: ${_qt_missing}\n" " Debian/Ubuntu: apt install qt6-base-dev qt6-base-dev-tools qt6-svg-dev libgl-dev\n" " macOS: brew install qt\n" " Windows: vcpkg install qtbase qtsvg\n" " or point CMAKE_PREFIX_PATH at a Qt installation.") endif() qt_standard_project_setup() set(HAVE_QT TRUE) set(HAVE_SGP TRUE) message(STATUS "Qt ${Qt6_VERSION}: building ctsim") endif() # --------------------------------------------------------------------------- # Platform probes (replace the autoconf checks) # --------------------------------------------------------------------------- # Only getopt.h is still consulted: every other header this loop probed for # guarded an include the tree no longer needs, and each probe was a chance # for a platform to be told something that then went unused. check_include_file(getopt.h HAVE_GETOPT_H) check_function_exists(getopt HAVE_GETOPT) check_function_exists(getopt_long CTSIM_SYSTEM_GETOPT_LONG) test_big_endian(WORDS_BIGENDIAN) # Upstream quirk: HAVE_GETOPT_LONG really means "use the bundled getopt/ directory" (phase 1.5 renames it) if(NOT CTSIM_SYSTEM_GETOPT_LONG) set(HAVE_GETOPT_LONG TRUE) endif() # The commit the program was built from, for the welcome message. Empty # when this is not a git checkout, which is the case for a source package. set(CTSIM_GIT_COMMIT "") find_package(Git QUIET) if(GIT_FOUND AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git") # The commit, not "git describe": describe names the nearest reachable # tag, which before a release is whatever milestone was tagged last, so # a 7.0.0 build reported itself as so many commits after 7.0.0~m1. The # message says "built from", and a commit is what that means. execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short=12 HEAD WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} OUTPUT_VARIABLE CTSIM_GIT_COMMIT OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) # Uncommitted changes: the commit alone would not describe the build. execute_process(COMMAND ${GIT_EXECUTABLE} diff --quiet HEAD WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} RESULT_VARIABLE _git_dirty ERROR_QUIET) if(NOT _git_dirty EQUAL 0 AND NOT CTSIM_GIT_COMMIT STREQUAL "") string(APPEND CTSIM_GIT_COMMIT "-dirty") endif() endif() configure_file(cmake/config.h.in "${CMAKE_BINARY_DIR}/config.h" @ONLY) # --------------------------------------------------------------------------- # Common interface target: flags + config.h + include/ # --------------------------------------------------------------------------- add_library(ctsim_common INTERFACE) target_include_directories(ctsim_common INTERFACE "${CMAKE_BINARY_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/include") target_compile_definitions(ctsim_common INTERFACE HAVE_CONFIG_H=1) # ct.h -- which every CTSim source includes -- includes png.h, zlib.h and # fftw3.h, so every target needs their include directories, not just the one # that calls into them. They are here rather than on ctsim_lib alone because # on Linux these headers are in /usr/include and the omission is invisible, # while on macOS with Homebrew (/opt/homebrew/include) and on Windows with # vcpkg the build fails with "png.h file not found" in libctsupport. target_link_libraries(ctsim_common INTERFACE PNG::PNG ZLIB::ZLIB PkgConfig::FFTW3) if(CTSIM_WARNINGS AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") # -Wvla and -Wunused-const-variable are what Apple clang reports by default # and GCC does not; asking GCC for them keeps the Linux build honest about # warnings that would otherwise only appear on macOS. The "=1" level form # is GCC's own -- clang has the flag without a level and rejects it with # one -- so each compiler gets its spelling. target_compile_options(ctsim_common INTERFACE -Wall -Wextra -Wno-unused-parameter -Wvla) if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") target_compile_options(ctsim_common INTERFACE -Wunused-const-variable=1) else() target_compile_options(ctsim_common INTERFACE -Wunused-const-variable) endif() elseif(CTSIM_WARNINGS AND MSVC) target_compile_options(ctsim_common INTERFACE /W3) endif() option(CTSIM_WERROR "Treat warnings as errors (CI)" OFF) # Address and undefined-behaviour sanitizers, for a checking build. Not a # default: they cost several times the run time and the regression suite # is timed. 6.2.0 reported the tree clean under both, but the rebinning # and PPM paths had no test then and were not exercised. option(CTSIM_SANITIZE "Build with AddressSanitizer and UBSan" OFF) if(CTSIM_WERROR AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") target_compile_options(ctsim_common INTERFACE -Werror) endif() if(CTSIM_SANITIZE) if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") # -fno-omit-frame-pointer so a report names the frames that matter. add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer -g) add_link_options(-fsanitize=address,undefined) # AddressSanitizer does not see an index past a vector's size but # within its capacity: the memory is the vector's own. libstdc++ # checks the index itself when asked, and several distributions build # every package that way, where such an index aborts the program. add_compile_definitions(_GLIBCXX_ASSERTIONS) message(STATUS "Sanitizers: address, undefined, libstdc++ assertions") else() message(WARNING "CTSIM_SANITIZE ignored: not GCC or Clang") endif() endif() if(CTSIM_NATIVE_ARCH AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") target_compile_options(ctsim_common INTERFACE -march=native) endif() if(HAVE_OPENMP) target_link_libraries(ctsim_common INTERFACE OpenMP::OpenMP_CXX) endif() if(MSVC) target_compile_definitions(ctsim_common INTERFACE MSVC=1 _CRT_SECURE_NO_WARNINGS NOMINMAX) endif() if(WIN32) target_link_libraries(ctsim_common INTERFACE ws2_32) # htonl/ntohl in fnetorderstream endif() # --------------------------------------------------------------------------- # Libraries # --------------------------------------------------------------------------- if(HAVE_GETOPT_LONG) add_library(ctgetopt STATIC getopt/getopt.c getopt/getopt1.c) # config.h lives in the build directory; getopt.h includes it by name. target_include_directories(ctgetopt PRIVATE ${CMAKE_BINARY_DIR}) target_include_directories(ctgetopt PUBLIC getopt) target_link_libraries(ctgetopt PRIVATE ctsim_common) endif() add_library(ctsupport STATIC libctsupport/strfuncs.cpp libctsupport/syserror.cpp libctsupport/fnetorderstream.cpp libctsupport/consoleio.cpp libctsupport/mathfuncs.cpp libctsupport/xform.cpp libctsupport/clip.cpp libctsupport/hashtable.cpp libctsupport/timeformat.cpp libctsupport/interpolator.cpp libctsupport/globalvars.cpp libctsupport/hooks.cpp) target_link_libraries(ctsupport PUBLIC ctsim_common) # 6.4.0: libctsupport is toolkit-neutral; it no longer includes src/ctsim.h or # links the GUI toolkit. Applications install handlers (see ctsupport_hooks.h). # Library target is ctsim_lib (output libctsim.a); the GUI executable owns the plain name "ctsim". add_library(ctsim_lib STATIC libctsim/filter.cpp libctsim/scanner.cpp libctsim/projections.cpp libctsim/phantom.cpp libctsim/imagefile.cpp libctsim/backprojectors.cpp libctsim/array2dfile.cpp libctsim/trace.cpp libctsim/procsignal.cpp libctsim/reconstruct.cpp libctsim/fourier.cpp libctsim/dicom.cpp libctsim/hdf5file.cpp libctsim/plotfile.cpp libctsim/pngmeta.cpp ) set_target_properties(ctsim_lib PROPERTIES OUTPUT_NAME ctsim) target_link_libraries(ctsim_lib PUBLIC ctsupport) # PNG, zlib and FFTW3 come from ctsim_common if(HAVE_GETOPT_LONG) target_link_libraries(ctsim_lib PUBLIC ctgetopt) endif() if(HAVE_DCMTK) target_link_libraries(ctsim_lib PUBLIC DCMTK::dcmdata DCMTK::ofstd DCMTK::oflog) endif() if(HAVE_HDF5) target_link_libraries(ctsim_lib PUBLIC ${HDF5_C_LIBRARIES}) target_include_directories(ctsim_lib PUBLIC ${HDF5_C_INCLUDE_DIRS}) endif() # libctgraphics: SGP (2-D canvas abstraction) + EZPlot. Needs a toolkit backend, so GUI builds only. if(HAVE_SGP) add_library(ctgraphics STATIC libctgraphics/ezplot.cpp libctgraphics/ezset.cpp libctgraphics/pol.cpp libctgraphics/sgp.cpp libctgraphics/transformmatrix.cpp) target_link_libraries(ctgraphics PUBLIC ctsupport) if(HAVE_QT) # The QPainter backend (M1). Only sgp_qt.cpp includes Qt headers. target_sources(ctgraphics PRIVATE libctgraphics/sgp_qt.cpp) target_link_libraries(ctgraphics PUBLIC Qt6::Gui) endif() target_link_libraries(ctsim_lib PUBLIC ctgraphics) # trace hooks in scanner/reconstruct/phantom endif() # --------------------------------------------------------------------------- # Command-line tools: one binary, busybox-style dispatch on argv[0] # --------------------------------------------------------------------------- # The functions and the shell that dispatches them, in a library of their # own so that ctsim and ctsim-nogui share one copy. Which one a person runs # should not change what a function does. add_library(ctsim_shell STATIC tools/shell.cpp tools/if1.cpp tools/if2.cpp tools/ifinfo.cpp tools/ifexport.cpp tools/phm2if.cpp tools/phm2pj.cpp tools/png2phm.cpp tools/pj2if.cpp tools/pjinfo.cpp tools/pjrec.cpp tools/phm2helix.cpp tools/pjHinterp.cpp tools/linogram.cpp tools/dicomimport.cpp tools/pjfourier.cpp tools/ctconvert.cpp tools/ctdump.cpp) target_link_libraries(ctsim_shell PUBLIC ctsim_lib) # So that a binary run from the build tree finds scripts/benchmarks. target_compile_definitions(ctsim_shell PRIVATE CTSIM_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}") add_executable(ctsim-nogui tools/ctsim-nogui.cpp) target_link_libraries(ctsim-nogui PRIVATE ctsim_shell) if(HAVE_READLINE) target_include_directories(ctsim_shell PUBLIC ${CTSIM_LINEEDIT_INCLUDE_DIR}) target_link_libraries(ctsim_shell PUBLIC ${CTSIM_LINEEDIT_LIBRARY}) endif() set(CTSIM_TOOL_ALIASES if1 if2 ifexport ifinfo phm2if png2phm phm2pj phm2helix pj2if pjinfo pjrec pjHinterp linogram dicomimport pjfourier ctconvert ctdump) # On Windows the executables name the libraries they need and CMake resolves # the closure; see the block at the end of this file. Everywhere else a # shared library is found through the system's own mechanism and nothing is # copied. if(WIN32) set(_ctsim_dep_set RUNTIME_DEPENDENCY_SET ctsim_windows_runtime) else() set(_ctsim_dep_set "") endif() install(TARGETS ctsim-nogui ${_ctsim_dep_set} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT Tools) if(WIN32) foreach(alias ${CTSIM_TOOL_ALIASES}) file(WRITE "${CMAKE_BINARY_DIR}/shims/${alias}.cmd" "@\"%~dp0ctsim-nogui.exe\" ${alias} %*\n") install(PROGRAMS "${CMAKE_BINARY_DIR}/shims/${alias}.cmd" DESTINATION ${CMAKE_INSTALL_BINDIR}) endforeach() else() # CMAKE_INSTALL_PREFIX is escaped so that it is read when the install # runs, where "cmake --install --prefix" has set it. The FULL_ form of # the directory is fixed at configure time, and sent the links to the # configured prefix whatever prefix the install was given. foreach(alias ${CTSIM_TOOL_ALIASES}) install(CODE "execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink ctsim-nogui \ \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}/${alias}\")") endforeach() endif() # Sample scripts. These were @prefix@ templates substituted by autoconf; with # autoconf gone they are configured here and installed alongside the # documentation. foreach(_sample tools/sample-ctsim) get_filename_component(_sample_name ${_sample} NAME) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/${_sample}.sh.in ${CMAKE_CURRENT_BINARY_DIR}/${_sample_name}.sh @ONLY) install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/${_sample_name}.sh DESTINATION ${CMAKE_INSTALL_DATADIR}/ctsim/examples COMPONENT Tools) endforeach() # The phantom generator phm2helix calls, one phantom per view. Source # rather than a binary: it is a page of C, and tests/helical.sh compiles it. install(FILES scripts/helical/dynphm.c DESTINATION ${CMAKE_INSTALL_DATADIR}/ctsim/examples COMPONENT Tools) # The demonstration script, which Help > Run Demonstration looks for. install(DIRECTORY scripts/benchmarks/ DESTINATION ${CMAKE_INSTALL_DATADIR}/ctsim/benchmarks COMPONENT Tools FILES_MATCHING PATTERN "*.bench") install(DIRECTORY scripts/demos/ DESTINATION ${CMAKE_INSTALL_DATADIR}/ctsim/demos COMPONENT Tools FILES_MATCHING PATTERN "*.ctsim") file(GLOB CTSIM_MAN_PAGES "${CMAKE_CURRENT_SOURCE_DIR}/man/*.1") install(FILES ${CTSIM_MAN_PAGES} DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 COMPONENT Tools) # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # The graphical application (src/qt/) # --------------------------------------------------------------------------- if(HAVE_QT) # The application is "ctsim". It was "ctsim-qt" through the port, while # the wxWidgets application still existed and owned the name. # # On Windows the executable carries an icon and version information, which # Explorer shows in a file's Properties; on macOS the bundle carries the # same icon as .icns. Both come from packaging/. set(_ctsim_platform_sources) if(WIN32) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/packaging/ctsim.rc.in ${CMAKE_BINARY_DIR}/ctsim.rc @ONLY) list(APPEND _ctsim_platform_sources ${CMAKE_BINARY_DIR}/ctsim.rc) elseif(APPLE) # The bundle's icon. Without it macOS shows a blank document icon, and # this was missing until 7.0.0~m8 because the file did not exist and the # rule below was silently skipped -- hence the check rather than a quiet # "if it is there". set(_ctsim_icns ${CMAKE_CURRENT_SOURCE_DIR}/packaging/ctsim.icns) if(NOT EXISTS ${_ctsim_icns}) message(FATAL_ERROR "packaging/ctsim.icns is missing; the application " "bundle would have no icon. Generate it from packaging/ctsim.svg: " "rsvg-convert -w N -h N packaging/ctsim.svg -o N.png for " "16 32 48 128 256 512, then png2icns (Linux) or iconutil (macOS).") endif() list(APPEND _ctsim_platform_sources ${_ctsim_icns}) set_source_files_properties(${_ctsim_icns} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) set(MACOSX_BUNDLE_ICON_FILE ctsim.icns) set(MACOSX_BUNDLE_BUNDLE_NAME CTSim) set(MACOSX_BUNDLE_GUI_IDENTIFIER org.ctsim.CTSim) set(MACOSX_BUNDLE_BUNDLE_VERSION "${CTSIM_VERSION_FULL}") set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}") set(MACOSX_BUNDLE_COPYRIGHT "Copyright (C) 1983-2026 Kevin M. Rosenberg") endif() qt_add_executable(ctsim WIN32 MACOSX_BUNDLE ${_ctsim_platform_sources} src/qt/main.cpp src/qt/app.cpp src/qt/mainwindow.cpp src/qt/shellwindow.cpp src/qt/document.cpp src/qt/documentwindow.cpp src/qt/scriptcaption.cpp src/qt/format.cpp src/qt/manual.cpp src/qt/printing.cpp src/qt/plotdocument.cpp src/qt/imagedocument.cpp src/qt/projectiondocument.cpp src/qt/phantomdocument.cpp src/qt/docmanager.cpp src/qt/scriptrunner.cpp src/qt/views/sgpcanvas.cpp src/qt/views/plotview.cpp src/qt/views/imageview.cpp src/qt/views/textview.cpp src/qt/views/projectionview.cpp src/qt/views/phantomview.cpp src/qt/views/graph3dview.cpp src/qt/dialogs/tracedialog.cpp src/qt/jobs/job.cpp src/qt/jobs/ctjobs.cpp src/qt/jobs/jobmanager.cpp src/qt/jobs/jobsdock.cpp src/qt/dialogs/scaledialogs.cpp src/qt/dialogs/imagedialogs.cpp src/qt/dialogs/propertiesdialog.cpp src/qt/dialogs/settingsdialog.cpp src/qt/dialogs/dialogmemory.cpp src/qt/dialogs/tipdialog.cpp src/qt/app.h src/qt/mainwindow.h src/qt/document.h src/qt/documentwindow.h src/qt/format.h src/qt/manual.h src/qt/printing.h src/qt/plotdocument.h src/qt/imagedocument.h src/qt/docmanager.h src/qt/projectiondocument.h src/qt/phantomdocument.h src/qt/scriptrunner.h src/qt/views/sgpcanvas.h src/qt/views/plotview.h src/qt/views/imageview.h src/qt/views/textview.h src/qt/views/projectionview.h src/qt/views/phantomview.h src/qt/views/graph3dview.h src/qt/dialogs/tracedialog.h src/qt/jobs/job.h src/qt/jobs/ctjobs.h src/qt/jobs/jobmanager.h src/qt/jobs/jobsdock.h src/qt/dialogs/scaledialogs.h src/qt/dialogs/imagedialogs.h src/qt/dialogs/propertiesdialog.h src/qt/dialogs/settingsdialog.h src/qt/dialogs/dialogmemory.h src/qt/dialogs/tipdialog.h src/shared/tipstrings.cpp) # Icons come straight from packaging/ so there is one copy in the tree. qt_add_resources(ctsim "ctsim_resources" PREFIX "/" BASE packaging FILES packaging/ctsim.svg packaging/icons/ctsim-256.png) target_include_directories(ctsim PRIVATE src/qt src/shared ${CMAKE_BINARY_DIR}) target_compile_definitions(ctsim PRIVATE CTSIM_INSTALL_DOCDIR="${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DOCDIR}") if(APPLE) # The 3-D view uses the fixed-function OpenGL API, which macOS has marked # deprecated since 10.14 but still ships. Apple's headers warn on every # call -- 29 of them -- and the only way to stop that short of rewriting # the view for shaders is this definition, which Qt's own examples use. target_compile_definitions(ctsim PRIVATE GL_SILENCE_DEPRECATION) endif() target_link_libraries(ctsim PRIVATE ctsim_shell ctsim_lib Qt6::Core Qt6::Gui Qt6::Widgets Qt6::OpenGLWidgets Qt6::PrintSupport Qt6::Svg OpenGL::GL) target_link_libraries(ctsim PRIVATE ctgraphics) # Install components: Runtime is the application, Tools the command-line # programs with their man pages and examples, Documentation the manual. # # On macOS the application is a bundle and its home is /Applications, not # /usr/local; "BUNDLE DESTINATION ." puts ctsim.app at the top of whatever # prefix is given, so # cmake --install build --component Runtime --prefix /Applications # is the manual install, and cpack's disk image has ctsim.app at its root. install(TARGETS ctsim ${_ctsim_dep_set} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT Runtime BUNDLE DESTINATION . COMPONENT Runtime) endif() if(HAVE_QT) # Desktop entry and icons, so the graphical program appears in menus. if(UNIX AND NOT APPLE) install(FILES packaging/ctsim.desktop COMPONENT Runtime DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) install(FILES packaging/ctsim.svg COMPONENT Runtime DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps) foreach(_size 16 22 24 32 48 64 128 256) install(FILES packaging/icons/ctsim-${_size}.png COMPONENT Runtime DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/${_size}x${_size}/apps RENAME ctsim.png) endforeach() endif() endif() # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- if(CTSIM_BUILD_TESTS) enable_testing() # Unit tests: pieces with analytically known behaviour (Siddon integrals, # file round trips, non-square images), as opposed to whole-pipeline goldens. add_executable(ctsim_unit_tests tests/unit_tests.cpp) target_link_libraries(ctsim_unit_tests PRIVATE ctsim_shell ctsim_lib) # Qt6::Test drives the shell window's keys; it exists only in a Qt build. if(CTSIM_GUI STREQUAL "qt") target_link_libraries(ctsim_unit_tests PRIVATE Qt6::Test) endif() # The plot-command reader lives in ctgraphics, which exists only when the # build has a canvas to draw on; its tests are compiled out otherwise. if(HAVE_SGP) target_link_libraries(ctsim_unit_tests PRIVATE ctgraphics) endif() if(HAVE_QT) # The SGP backend test renders offscreen; it needs a QGuiApplication. # The M2 tests construct real widgets, so Qt6::Widgets as well, and they # compile the Qt sources they exercise. target_sources(ctsim_unit_tests PRIVATE src/qt/document.cpp src/qt/documentwindow.cpp src/qt/scriptcaption.cpp src/qt/format.cpp src/qt/manual.cpp src/qt/printing.cpp src/qt/manual.cpp src/qt/format.cpp src/qt/imagedocument.cpp src/qt/plotdocument.cpp src/qt/projectiondocument.cpp src/qt/phantomdocument.cpp src/qt/docmanager.cpp src/qt/scriptrunner.cpp src/qt/app.cpp src/qt/views/imageview.cpp src/qt/views/sgpcanvas.cpp src/qt/views/plotview.cpp src/qt/views/textview.cpp src/qt/views/projectionview.cpp src/qt/views/phantomview.cpp src/qt/mainwindow.cpp src/qt/shellwindow.cpp src/qt/jobs/job.cpp src/qt/jobs/ctjobs.cpp src/qt/jobs/jobmanager.cpp src/qt/jobs/jobsdock.cpp src/qt/views/graph3dview.cpp src/qt/dialogs/tracedialog.cpp src/qt/dialogs/scaledialogs.cpp src/qt/dialogs/imagedialogs.cpp src/qt/dialogs/propertiesdialog.cpp src/qt/dialogs/settingsdialog.cpp src/qt/dialogs/dialogmemory.cpp src/qt/dialogs/tipdialog.cpp src/shared/tipstrings.cpp) target_compile_definitions(ctsim_unit_tests PRIVATE CTSIM_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}") if(APPLE) target_compile_definitions(ctsim_unit_tests PRIVATE GL_SILENCE_DEPRECATION) endif() target_include_directories(ctsim_unit_tests PRIVATE src/qt src/shared) # The tests construct the main window, which uses the application icons; # without the resources linked Qt logs a missing-file warning on every run. qt_add_resources(ctsim_unit_tests "ctsim_test_resources" PREFIX "/" BASE packaging FILES packaging/ctsim.svg packaging/icons/ctsim-256.png) # ctgraphics is linked by the HAVE_SGP block above, which is true # whenever HAVE_QT is; naming it here as well made the linker report a # duplicate library. target_link_libraries(ctsim_unit_tests PRIVATE Qt6::Gui Qt6::Widgets Qt6::OpenGLWidgets Qt6::PrintSupport OpenGL::GL) endif() # One ctest entry per group, so ctest names what ran and a group can be run # alone. The ids are the ones ctsim_unit_tests --list prints. set(CTSIM_UNIT_GROUPS siddon phantom image nonsquare netorder plotfile plothdf5 pngphm hdf5 history) if(HAVE_QT) list(APPEND CTSIM_UNIT_GROUPS sgpqt imageview projphm jobs graph3d trace dialogmem helpclip) endif() foreach(_group ${CTSIM_UNIT_GROUPS}) add_test(NAME unit.${_group} COMMAND ctsim_unit_tests --group ${_group} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) endforeach() if(HAVE_QT) # The Qt group renders offscreen and needs a platform plugin. # Only these two build widgets; the others never touch Qt at run time. set_tests_properties(unit.sgpqt unit.imageview unit.projphm unit.jobs unit.graph3d unit.trace unit.dialogmem unit.helpclip PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen;QT_QPA_OFFSCREEN_NO_GLX=1" ENVIRONMENT_MODIFICATION "DISPLAY=unset:") endif() # The tool man pages are generated from the tools' own --help (see # man/sync-options.py); this fails when a tool's options change without the # pages being regenerated. find_package(Python3 COMPONENTS Interpreter QUIET) if(Python3_Interpreter_FOUND) add_test(NAME man_pages COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/man/sync-options.py --check $) endif() # Every function the shell offers must also be installed under its own # name. png2phm was missing from CTSIM_TOOL_ALIASES and so had no symlink # in the package, which nothing caught: the function worked through the # shell, and only an installed tree showed the gap. add_test(NAME tool_aliases COMMAND sh ${CMAKE_CURRENT_SOURCE_DIR}/tests/check-aliases.sh $ ${CTSIM_TOOL_ALIASES}) # The helical chain -- phm2helix, pjHinterp, pjrec -- which nothing else # exercises. It builds scripts/helical/dynphm.c, and where there is no # compiler to build it with it exits 77, so ctest reports a skip and not # a pass. add_test(NAME helical COMMAND sh ${CMAKE_CURRENT_SOURCE_DIR}/tests/helical.sh $ ${CMAKE_C_COMPILER}) set_tests_properties(helical PROPERTIES SKIP_RETURN_CODE 77) # The install rules name files by path, and nothing in the build reads # those paths: a file that moves leaves the build and every other test # green and "cmake --install" broken, which is how a package build finds # out. So install into a scratch prefix. Linux only: on Windows and # macOS the install step deploys Qt and signs, which is packaging work # and not a test. if(UNIX AND NOT APPLE) add_test(NAME install_tree COMMAND ${CMAKE_COMMAND} --install ${CMAKE_BINARY_DIR} --prefix ${CMAKE_BINARY_DIR}/install-check) endif() if(HAVE_QT) # GUI parity: the same operations run through the Qt application and # through ctsim-nogui must produce identical files (tests/gui-parity.sh). add_test(NAME gui_parity COMMAND sh ${CMAKE_CURRENT_SOURCE_DIR}/tests/gui-parity.sh $ $ WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) set_tests_properties(gui_parity PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen;QT_QPA_OFFSCREEN_NO_GLX=1" ENVIRONMENT_MODIFICATION "DISPLAY=unset:") # The same operations in sequence, driven by a script, against the same # pipeline run with ctsim-nogui. add_test(NAME gui_sequence COMMAND sh ${CMAKE_CURRENT_SOURCE_DIR}/tests/gui-sequence.sh $ $ WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) set_tests_properties(gui_sequence PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen;QT_QPA_OFFSCREEN_NO_GLX=1" ENVIRONMENT_MODIFICATION "DISPLAY=unset:") endif() # Named for what it is. It was "herman_suite" when it did nothing but # compare herman reconstructions against golden files; it now covers the # raster phantoms, DICOM, the Fourier reconstructions and the unit tests # as well, and a failure in any of those was reported under the name of a # phantom that had nothing to do with it. add_test(NAME regression_suite COMMAND sh ${CMAKE_CURRENT_SOURCE_DIR}/tests/run-tests.sh run $) set_tests_properties(regression_suite PROPERTIES ENVIRONMENT "CTSIM_REQUIRE_RASTER=1;CTSIM_SKIP_PYTHON_TESTS=1") # The built binary must agree with the newest ChangeLog entry. Its own # test, because it asks a question about the release rather than about a # reconstruction, and a failure should say so in its name. add_test(NAME version_changelog COMMAND sh ${CMAKE_CURRENT_SOURCE_DIR}/tests/run-tests.sh version $) # Physics validation: CTSim's reconstruction of the raw DICOM scan against the # scanner's own reconstruction. Registered separately so it can be run alone, # and so a missing python3/numpy shows up as "skipped" rather than a failure. find_package(Python3 COMPONENTS Interpreter QUIET) if(Python3_Interpreter_FOUND) add_test(NAME dicom_physics_validation COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/validate_dicom_recon.py $) set_tests_properties(dicom_physics_validation PROPERTIES SKIP_RETURN_CODE 77) endif() endif() # --------------------------------------------------------------------------- # Manual (6.4.0: Markdown + pandoc, replacing the unmaintained tex2rtf) # --------------------------------------------------------------------------- # The manual is built from doc/manual/*.md by pandoc; the PDF additionally # needs a LaTeX engine and rsvg-convert for the figures. Each is looked for # and reported, because without pandoc the "manual" targets do not exist at # all and "cmake --build . --target manual" then fails with nothing more # helpful than "unknown target". # A manual built elsewhere and committed to the tree. Used when pandoc is # not installed, which is every Windows machine and any minimal build host. set(CTSIM_MANUAL_ARTIFACTS "${CMAKE_CURRENT_SOURCE_DIR}/artifacts/manual" CACHE PATH "A prebuilt manual to use when pandoc is not available") find_program(PANDOC_EXECUTABLE pandoc) find_program(PDFLATEX_EXECUTABLE pdflatex HINTS /Library/TeX/texbin /usr/local/texlive/bin) find_program(RSVG_CONVERT_EXECUTABLE rsvg-convert) if(PANDOC_EXECUTABLE) # How to ask for MathML. pandoc spells this --mathml up to 3.1 and # --math-method=mathml after, deprecating the old name; neither accepts # the other. Asked here rather than assumed, because the answer differs # between the pandoc on a current Ubuntu and the one on a current macOS. # The probe converts a real document: --version returns before the # option is parsed, so it would accept anything. file(WRITE "${CMAKE_BINARY_DIR}/pandoc-probe.md" "probe\n") execute_process( COMMAND ${PANDOC_EXECUTABLE} --math-method=mathml -f markdown -t html -o ${CMAKE_BINARY_DIR}/pandoc-probe.html ${CMAKE_BINARY_DIR}/pandoc-probe.md RESULT_VARIABLE _pandoc_math_new OUTPUT_QUIET ERROR_QUIET) if(_pandoc_math_new EQUAL 0) set(CTSIM_PANDOC_MATH "--math-method=mathml") else() set(CTSIM_PANDOC_MATH "--mathml") endif() message(STATUS "Manual: MathML via ${CTSIM_PANDOC_MATH}") # The LaTeX fragment needs the icon's absolute path, so it is configured. set(CTSIM_LOGO_PDF "${CMAKE_BINARY_DIR}/manual/ctsim-logo.pdf") configure_file(doc/manual/logo.tex.in "${CMAKE_BINARY_DIR}/manual-logo.tex" @ONLY) set(CTSIM_MANUAL_CHAPTERS concepts install gui textui algorithms sgp) set(_manual_sources) foreach(_chapter ${CTSIM_MANUAL_CHAPTERS}) list(APPEND _manual_sources "${CMAKE_CURRENT_SOURCE_DIR}/doc/manual/${_chapter}.md") endforeach() add_custom_target(manual COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/manual # The HTML refers to figures/ relative to itself, so they travel with it. COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/doc/manual/figures ${CMAKE_BINARY_DIR}/manual/figures # The logo the fragment refers to, beside the HTML that refers to it. COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/packaging/ctsim.svg ${CMAKE_BINARY_DIR}/manual/ctsim-logo.svg COMMAND ${PANDOC_EXECUTABLE} --standalone --toc --metadata=title:CTSim-${CTSIM_VERSION_FULL}-User-Manual --resource-path=${CMAKE_CURRENT_SOURCE_DIR}/doc/manual ${CTSIM_PANDOC_MATH} --include-before-body=${CMAKE_CURRENT_SOURCE_DIR}/doc/manual/logo.html -o ${CMAKE_BINARY_DIR}/manual/ctsim.html ${_manual_sources} DEPENDS ${_manual_sources} COMMENT "Building the manual (HTML) into ${CMAKE_BINARY_DIR}/manual") add_custom_target(manual-pdf COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/manual COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/doc/manual/figures ${CMAKE_BINARY_DIR}/manual/figures # --resource-path lets pandoc find figures/ from the source tree whatever # the working directory; the SVG figures are converted by rsvg-convert on # the way into LaTeX. See doc/manual/README.md for the packages needed. # pdflatex cannot read SVG, so the icon is converted once here; the # LaTeX fragment includes the result by name. COMMAND rsvg-convert --format=pdf --width=256 --output=${CMAKE_BINARY_DIR}/manual/ctsim-logo.pdf ${CMAKE_CURRENT_SOURCE_DIR}/packaging/ctsim.svg COMMAND ${PANDOC_EXECUTABLE} --standalone --toc --number-sections --pdf-engine=pdflatex --resource-path=${CMAKE_CURRENT_SOURCE_DIR}/doc/manual --resource-path=${CMAKE_BINARY_DIR}/manual --metadata=title:CTSim-${CTSIM_VERSION_FULL}-User-Manual -V geometry:margin=1in -V colorlinks=true --include-in-header=${CMAKE_BINARY_DIR}/manual-logo.tex -o ${CMAKE_BINARY_DIR}/manual/ctsim.pdf ${_manual_sources} DEPENDS ${_manual_sources} COMMENT "Building the manual (PDF); needs pdflatex and rsvg-convert, see doc/manual/README.md") ctsim_install_hint(_rsvg_hint librsvg2-bin librsvg librsvg) if(PDFLATEX_EXECUTABLE AND RSVG_CONVERT_EXECUTABLE) message(STATUS "Manual: targets \"manual\" (HTML) and \"manual-pdf\" (PDF)") elseif(PDFLATEX_EXECUTABLE) message(STATUS "Manual: targets \"manual\" and \"manual-pdf\"; rsvg-convert not " "found, so the PDF will have no figures " "(${_rsvg_hint})") else() message(STATUS "Manual: target \"manual\" (HTML). No LaTeX engine, so " "\"manual-pdf\" will fail " "(brew install basictex, then add /Library/TeX/texbin to PATH | " "apt install texlive-latex-recommended lmodern)") endif() elseif(EXISTS "${CTSIM_MANUAL_ARTIFACTS}/ctsim.html") # No pandoc, but a manual built earlier is in the tree. It is the same on # every platform, so a copy is as good as a build -- and this is the only # way to package a manual on Windows, where pandoc is not available at all. # # Part of the default build rather than a target to be asked for: the # point of it is that a package built with no thought about the manual # still has one. It is a file copy, so it costs nothing. add_custom_target(manual ALL COMMAND ${CMAKE_COMMAND} -E copy_directory "${CTSIM_MANUAL_ARTIFACTS}" "${CMAKE_BINARY_DIR}/manual" COMMENT "Copying the prebuilt manual from artifacts/manual (pandoc not found)") # manual-pdf is the same copy, so that whatever asks for either gets the # manual rather than an error about a target that does not exist. add_custom_target(manual-pdf DEPENDS manual) set(_manual_have "") foreach(_f ctsim.html ctsim.pdf) if(EXISTS "${CTSIM_MANUAL_ARTIFACTS}/${_f}") string(APPEND _manual_have " ${_f}") endif() endforeach() message(STATUS "Manual: pandoc not found; using the prebuilt" "${_manual_have} from artifacts/manual") else() ctsim_install_hint(_pandoc_hint pandoc pandoc "") if(_pandoc_hint STREQUAL "") set(_pandoc_hint "MSYS2 has no pandoc package; install it from https://pandoc.org") endif() message(STATUS "Manual: pandoc not found and artifacts/manual is not there, " "so there is no manual to install (${_pandoc_hint}); " "see doc/manual/README.md") endif() # Installed when the files are there, whether this build made them or not. # The manual is the same on every platform, so it can be built once -- on a # machine that has pandoc -- and its output copied into ${CMAKE_BINARY_DIR} # to be packaged somewhere that has not. OPTIONAL: a build without it # installs the rest and the Help menu reports that it found nothing. # Which manual gets installed is decided when install runs, not when cmake # configures, because the build directory's manual may be made after this # file is read -- "cmake --build . --target manual" is a separate step, and # with pandoc present the manual target is not part of the default build. # The rule: whatever is in the build directory wins; failing that, the # prebuilt manual in artifacts/manual. Configuring on the pandoc test # instead meant that a machine with pandoc, where the manual had not been # asked for, packaged no manual at all even though artifacts/manual held # one. install(CODE " set(_src \"${CMAKE_BINARY_DIR}/manual\") set(_from \"the build directory\") if(NOT EXISTS \"\${_src}/ctsim.html\" AND NOT EXISTS \"\${_src}/ctsim.pdf\") set(_src \"${CTSIM_MANUAL_ARTIFACTS}\") set(_from \"artifacts/manual\") endif() # Built from CMAKE_INSTALL_PREFIX as the install script sees it, not from # the absolute path this file computed: cpack redirects the prefix to a # staging directory, and a hardcoded path writes outside the package. set(_doc \"\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DOCDIR}\") set(_any OFF) foreach(_f ctsim.html ctsim.pdf ctsim-logo.svg) # the logo the HTML refers to if(EXISTS \"\${_src}/\${_f}\") file(INSTALL \"\${_src}/\${_f}\" DESTINATION \"\${_doc}\") set(_any ON) endif() endforeach() if(EXISTS \"\${_src}/figures\") file(INSTALL \"\${_src}/figures\" DESTINATION \"\${_doc}\") endif() if(_any) message(STATUS \"Manual installed from \${_from}\") else() message(STATUS \"No manual to install: neither the build directory nor \" \"artifacts/manual has one\") endif() " COMPONENT Documentation) # --------------------------------------------------------------------------- # Packaging # --------------------------------------------------------------------------- set(CPACK_PACKAGE_NAME ctsim) set(CPACK_PACKAGE_VERSION "${CTSIM_VERSION_FULL}") # 7.0.0~m0: Debian orders the tilde correctly set(CPACK_PACKAGE_VENDOR "Kevin M. Rosenberg") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Computed tomography simulator") set(CPACK_PACKAGE_CONTACT "Kevin M. Rosenberg ") # The agreement page both installers show. As plain COPYING it began in # the middle of a sentence about the Herman phantom and never said what was # being installed or by whom; NSIS and the macOS disk image both render # RTF, so it is written out with the logo and a heading above the terms. set(_ctsim_license_rtf "${CMAKE_BINARY_DIR}/ctsim-license.rtf") set(_ctsim_license_args -DCOPYING_IN=${CMAKE_CURRENT_SOURCE_DIR}/COPYING -DLOGO_IN=${CMAKE_CURRENT_SOURCE_DIR}/packaging/license-logo.png -DRTF_OUT=${_ctsim_license_rtf} -DCTSIM_VERSION=${CTSIM_VERSION_FULL} -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/make-license-rtf.cmake) # Written now, because CPack checks at configure time that the licence it # has been given exists. Written again at build time by the rule below, so # that editing COPYING does not need cmake to be run again. execute_process(COMMAND ${CMAKE_COMMAND} ${_ctsim_license_args} RESULT_VARIABLE _license_result) if(NOT _license_result EQUAL 0) message(FATAL_ERROR "could not write ${_ctsim_license_rtf}") endif() add_custom_command(OUTPUT "${_ctsim_license_rtf}" COMMAND ${CMAKE_COMMAND} ${_ctsim_license_args} DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/COPYING" "${CMAKE_CURRENT_SOURCE_DIR}/packaging/license-logo.png" "${CMAKE_CURRENT_SOURCE_DIR}/cmake/make-license-rtf.cmake" COMMENT "Writing the installer licence") add_custom_target(ctsim_license_rtf ALL DEPENDS "${_ctsim_license_rtf}") set(CPACK_RESOURCE_FILE_LICENSE "${_ctsim_license_rtf}") set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) set(CPACK_DEBIAN_PACKAGE_SECTION science) set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "http://www.ctsim.org/") set(CPACK_GENERATOR "TGZ") # Packages, and cpack's _CPack_Packages staging directory, go to the build # directory. cpack's own default is the directory it is run from, which for # "cpack --config build/CPackConfig.cmake" at the top of the tree means the # source tree -- a disk image and a staging tree dropped among the sources. set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}") # CPack also builds source tarballs: "cmake --build build --target package_source" # produces one from the working tree, excluding build directories and editor # leftovers. This is separate from the binary packages above. # ZIP as well: it is what a Windows machine opens without being given a # tool, and what a release page is usually asked for alongside the tarball. set(CPACK_SOURCE_GENERATOR "TGZ;TXZ;ZIP") set(CPACK_SOURCE_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CTSIM_VERSION_FULL}") set(CPACK_SOURCE_IGNORE_FILES "/build.*/" "/\\\\.git/" "/\\\\.gitlab-ci\\\\.yml$" "~$" "\\\\.o$" "\\\\.a$" "/CMakeCache\\\\.txt$" "/CMakeFiles/" "/__pycache__/" "\\\\.pyc$" "/_CPack_Packages/" # cpack's own staging, when run inside the tree "/\\\\.vscode/" "/\\\\.idea/" "/\\\\.DS_Store$" "/b/" # the usual build directory "/\\\\.cache/") if(CMAKE_SYSTEM_NAME STREQUAL "Linux") list(APPEND CPACK_GENERATOR DEB) endif() # Relocatable packages for macOS and Windows. A Qt application cannot simply # be copied to another machine: the Qt libraries and the platform plugin have # to travel with it. Qt's own deployment support does that at install time, # and it deploys only the Qt modules and plugins the application actually # links. # # macdeployqt still reports that it cannot resolve QtPdf and # QtVirtualKeyboard on a Homebrew machine. Those are named by plugins Qt # ships, not by anything CTSim uses, and Homebrew keeps them in formulae # separate from qtbase; the messages say ERROR but deployment carries on and # the bundle runs. Installing the full "qt" formula rather than qtbase and # qtsvg silences them. if(HAVE_QT AND (APPLE OR WIN32)) qt_generate_deploy_app_script(TARGET ctsim OUTPUT_SCRIPT _ctsim_deploy_script NO_UNSUPPORTED_PLATFORM_ERROR NO_TRANSLATIONS) install(SCRIPT ${_ctsim_deploy_script} COMPONENT Runtime) endif() if(HAVE_QT AND APPLE) # A Mac is given one thing to drag, so the bundle carries everything: the # application, the command-line tools, their manual pages, and the manual # the Help menu opens (which it looks for in Contents/Resources/manual). # Without this a drag-installed CTSim has no manual and no ctsim-nogui. install(TARGETS ctsim-nogui RUNTIME DESTINATION ctsim.app/Contents/MacOS COMPONENT Runtime) install(FILES ${CTSIM_MAN_PAGES} DESTINATION ctsim.app/Contents/Resources/man/man1 COMPONENT Runtime) install(FILES ${CMAKE_BINARY_DIR}/manual/ctsim.pdf ${CMAKE_BINARY_DIR}/manual/ctsim.html DESTINATION ctsim.app/Contents/Resources/manual OPTIONAL COMPONENT Runtime) install(DIRECTORY ${CMAKE_BINARY_DIR}/manual/figures DESTINATION ctsim.app/Contents/Resources/manual OPTIONAL COMPONENT Runtime) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/packaging/macos-command-line-tools.txt DESTINATION . COMPONENT Runtime RENAME "Command-line tools.txt") install(DIRECTORY scripts/benchmarks/ DESTINATION ctsim.app/Contents/Resources/benchmarks FILES_MATCHING PATTERN "*.bench") install(DIRECTORY scripts/demos/ DESTINATION ctsim.app/Contents/Resources/demos COMPONENT Runtime FILES_MATCHING PATTERN "*.ctsim") # "cpack -G DragNDrop" then makes a disk image with ctsim.app at its root. list(APPEND CPACK_GENERATOR DragNDrop) set(CPACK_DMG_VOLUME_NAME "CTSim ${CTSIM_VERSION_FULL}") # The window the mounted image opens in. macOS does not draw the picture # in an RTF licence -- the agreement it shows is text, and the logo in the # licence appears on Windows but not here -- so this is where a Mac user # sees the logo. set(CPACK_DMG_BACKGROUND_IMAGE "${CMAKE_CURRENT_SOURCE_DIR}/packaging/dmg-background.png") # The licence set above as CPACK_RESOURCE_FILE_LICENSE becomes the # agreement the disk image shows when it is opened; CPack embeds it for # every language and supplies the buttons itself. # # Not CPACK_DMG_SLA_DIR: that asks for a directory holding both a licence # and a menu file naming the buttons for each language, and without the # menu file the generator refuses to start. Given both, CPack says it is # ignoring the directory and then still demands the menu file from it. set(CPACK_PACKAGING_INSTALL_PREFIX "/") set(CPACK_COMPONENTS_ALL Runtime) endif() if(WIN32) # The libraries the executables were linked against, copied beside them. # windeployqt (above) brings Qt's own and its plugins; it knows nothing # about the compiler runtime, or FFTW, or DCMTK, or HDF5 -- which is why # an installed CTSim reported libgcc_s_seh-1.dll, then libstdc++-6.dll, # then libfftw3-3.dll, then libdcmdata.dll, one at a time. CMake reads # the executables and follows what they ask for. # # DIRECTORIES is where to look: the compiler's own bin directory, which # under MSYS2 is also where every package puts its DLLs. get_filename_component(_ctsim_compiler_bin "${CMAKE_CXX_COMPILER}" DIRECTORY) # Windows' own libraries are found so that they can be recognised and # left behind. Without the system directory to look in, every one of # them -- kernel32, user32, dwrite, dxgi and a long tail besides -- is # searched for among the MinGW libraries, not found, and reported as # unresolved, which is a great many warnings for a question nobody asked. # POST_EXCLUDE then drops whatever was found there, so none is copied: # a program must use the system's own, not a copy in its own directory. file(TO_CMAKE_PATH "$ENV{SystemRoot}" _ctsim_system_root) if(NOT _ctsim_system_root) set(_ctsim_system_root "C:/Windows") endif() install(RUNTIME_DEPENDENCY_SET ctsim_windows_runtime PRE_EXCLUDE_REGEXES "api-ms-win-.*" "ext-ms-.*" # Spelled out letter by letter because CMake's regexes have no # case-insensitive flag -- "(?i)" does not compile -- and Windows # reports its own directory as WINDOWS, Windows or windows # depending on who is asking. POST_EXCLUDE_REGEXES ".*[/\\][Ww][Ii][Nn][Dd][Oo][Ww][Ss][/\\].*" DIRECTORIES "${_ctsim_compiler_bin}" "${_ctsim_system_root}/System32" "${_ctsim_system_root}/SysWOW64" DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT Runtime) endif() if(HAVE_QT AND WIN32) list(APPEND CPACK_GENERATOR NSIS) set(CPACK_NSIS_PACKAGE_NAME "CTSim") set(CPACK_NSIS_DISPLAY_NAME "CTSim ${CTSIM_VERSION_FULL}") set(CPACK_NSIS_INSTALLED_ICON_NAME "bin\\\\ctsim.exe") set(CPACK_NSIS_MUI_ICON "${CMAKE_CURRENT_SOURCE_DIR}/packaging/ctsim.ico") set(CPACK_NSIS_MUI_UNIICON "${CMAKE_CURRENT_SOURCE_DIR}/packaging/ctsim.ico") # The logo on every page, and beside the welcome and finish pages. set(CPACK_NSIS_MUI_HEADERIMAGE "${CMAKE_CURRENT_SOURCE_DIR}/packaging/nsis-header.bmp") set(CPACK_NSIS_MUI_WELCOMEFINISHPAGE_BITMAP "${CMAKE_CURRENT_SOURCE_DIR}/packaging/nsis-welcome.bmp") set(CPACK_NSIS_MUI_UNWELCOMEFINISHPAGE_BITMAP "${CMAKE_CURRENT_SOURCE_DIR}/packaging/nsis-welcome.bmp") set(CPACK_NSIS_HELP_LINK "http://www.ctsim.org/") set(CPACK_NSIS_URL_INFO_ABOUT "http://www.ctsim.org/") set(CPACK_NSIS_MODIFY_PATH ON) set(CPACK_NSIS_CREATE_ICONS_EXTRA "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\CTSim.lnk' '$INSTDIR\\\\bin\\\\ctsim.exe'") set(CPACK_NSIS_DELETE_ICONS_EXTRA "Delete '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\CTSim.lnk'") endif() include(CPack) message(STATUS "CTSim ${PROJECT_VERSION}: GUI=${CTSIM_GUI} OpenMP=${HAVE_OPENMP} " "readline=${HAVE_READLINE} DICOM=${HAVE_DCMTK} " "bundled-getopt=${HAVE_GETOPT_LONG}") if(HAVE_QT AND APPLE) # A Qt application on macOS is built as a bundle, so there is no plain # "ctsim" file in the build directory to look for. message(STATUS " the application is built as ${CMAKE_BINARY_DIR}/ctsim.app; " "run it with \"open ctsim.app\" or " "ctsim.app/Contents/MacOS/ctsim --version") endif() ctsim-7.0.0/cmake/0000755000175000017500000000000015253543770012742 5ustar kevinkevinctsim-7.0.0/cmake/make-license-rtf.cmake0000644000175000017500000000543515253543770017101 0ustar kevinkevin# Writes the licence the Windows installer shows, as RTF. # # The installer's agreement page showed COPYING as plain text, which begins # in the middle of a sentence about the Herman phantom and never says what # is being installed or by whom. NSIS renders RTF on that page, so the # licence carries a heading: the logo, the program, its author, the website # and the copyright, above the terms. Both NSIS and the macOS disk image # render RTF on that page. # # Run at configure time: # cmake -DCOPYING_IN=... -DLOGO_IN=... -DRTF_OUT=... -DCTSIM_VERSION=... -P this-file if(NOT COPYING_IN OR NOT RTF_OUT) message(FATAL_ERROR "COPYING_IN and RTF_OUT are required") endif() file(READ "${COPYING_IN}" _text) # RTF's own three metacharacters, before anything else is inserted. string(REPLACE "\\" "\\\\" _text "${_text}") string(REPLACE "{" "\\{" _text "${_text}") string(REPLACE "}" "\\}" _text "${_text}") # Anything above 7 bits, as RTF cannot carry it literally. The GPL has a # few: they become the nearest plain equivalent rather than a mojibake. string(REGEX REPLACE "[^\r\n\t -~]" "?" _text "${_text}") # A blank line starts a paragraph; a single newline is a line break. Doing # the blank lines first stops the second rule from eating them. string(REGEX REPLACE "\r?\n[ \t]*\r?\n" "\\\\par\\\\par\n" _text "${_text}") string(REGEX REPLACE "\r?\n" "\\\\line\n" _text "${_text}") # The logo, carried in the file as hex. \picwgoal and \pichgoal are twips, # 1440 to the inch, so 120 pixels at 96 dpi is 1800 of them. A reader that # cannot show the picture skips the group and loses only the picture. set(_logo "") if(LOGO_IN AND EXISTS "${LOGO_IN}") file(READ "${LOGO_IN}" _logo_hex HEX) # Broken into lines. RTF ignores whitespace inside the hex of a picture, # and the macOS disk image will not take a licence holding any run of # more than 255 characters without a break -- which one picture on one # line certainly is. string(REGEX REPLACE "(................................................................)" "\\1\n" _logo_hex "${_logo_hex}") set(_logo "{\\pict\\pngblip\\picw120\\pich120\\picwgoal1800\\pichgoal1800\n${_logo_hex}}\\par\n") endif() # \fs is half-points: fs40 is 20pt, fs16 is 8pt. set(_rtf "{\\rtf1\\ansi\\ansicpg1252\\deff0 {\\fonttbl{\\f0\\fswiss\\fcharset0 Arial;}} {\\colortbl;\\red44\\green74\\blue112;\\red120\\green84\\blue0;\\red0\\green0\\blue0;} \\pard\\qc ${_logo}\\cf1\\b\\fs40 CTSim ${CTSIM_VERSION}\\b0\\par \\cf1\\fs22 Computed tomography simulator\\par \\fs20\\par \\cf3\\fs28\\b Kevin M. Rosenberg, MD\\b0\\par \\cf2\\b\\fs22 www.ctsim.org\\b0\\par \\cf3\\fs18 Copyright (c) 1983-2026 Kevin Rosenberg\\par \\fs18 Released under the GNU General Public License\\par \\fs16\\par \\pard\\ql\\li1100\\ri1100\\fs16 ${_text} } ") file(WRITE "${RTF_OUT}" "${_rtf}") ctsim-7.0.0/cmake/config.h.in0000644000175000017500000000247115253543770014771 0ustar kevinkevin/* config.h generated by CMake from cmake/config.h.in — replaces the autoconf config.h.in */ #ifndef CTSIM_CONFIG_H #define CTSIM_CONFIG_H #define VERSION "@CTSIM_VERSION_FULL@" /* e.g. 7.0.0~m0; the Debian-ordered string */ #define CTSIM_GIT_COMMIT "@CTSIM_GIT_COMMIT@" /* empty outside a checkout */ /* Optional libraries / features */ #define HAVE_FFTW 1 /* mandatory */ #define HAVE_PNG 1 /* mandatory */ #cmakedefine HAVE_READLINE 1 #cmakedefine HAVE_STIFLE_HISTORY #cmakedefine HAVE_READLINE_READLINE_H 1 #cmakedefine HAVE_EDITLINE_READLINE_H 1 #cmakedefine HAVE_OPENMP 1 #cmakedefine HAVE_DCMTK 1 /* DICOM import/export via DCMTK */ #cmakedefine HAVE_HDF5 1 /* HDF5 file format (default for new files) */ #cmakedefine HAVE_QT 1 /* Qt 6 GUI (src/qt/) */ #cmakedefine HAVE_SGP 1 /* Platform headers. Only the ones that are genuinely optional: the C++ standard library is not, and probing for on a C++20 compiler answered a question nobody asks. */ #cmakedefine HAVE_GETOPT_H 1 /* Functions */ #cmakedefine HAVE_GETOPT 1 /* NOTE: upstream semantics are inverted — HAVE_GETOPT_LONG means "use the bundled getopt/" */ #cmakedefine HAVE_GETOPT_LONG 1 /* Type sizes and endianness */ #cmakedefine WORDS_BIGENDIAN 1 #endif /* CTSIM_CONFIG_H */ ctsim-7.0.0/COPYING0000644000175000017500000010524215253543770012721 0ustar kevinkevinCTSim is covered by the GNU GPL stated below. Additionally, the Herman Phantom defined in libctsim/phantom.cpp is copyright by Gabor T. Herman, Ph.D. Dr. Herman has granted permission for use of the his phantom in CTSim and derivative software products. However, any print or web publication of his phantom requries his explicit approval. GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ctsim-7.0.0/libctsupport/0000755000175000017500000000000015253666241014413 5ustar kevinkevinctsim-7.0.0/libctsupport/strfuncs.cpp0000644000175000017500000000065415253543770016774 0ustar kevinkevin#include "ctsupport.h" /* NAME * str_skip_head Skip leading characters * * SYNOPSIS * sv = str_skip_head (str, charlist) * * The view returned is a window on to str, not a copy of it. */ std::string_view str_skip_head (std::string_view str, std::string_view charlist) { const std::size_t i = str.find_first_not_of (charlist); return (i == std::string_view::npos) ? std::string_view() : str.substr (i); } ctsim-7.0.0/libctsupport/interpolator.cpp0000644000175000017500000000731115253543770017644 0ustar kevinkevin/***************************************************************************** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #include "ctsupport.h" #include "interpolator.h" CubicPolyInterpolator::CubicPolyInterpolator (const double* const y, const int n) : m_pdY(y), m_n(n) { if (m_n < 2) sys_error (ERR_SEVERE, "Too few points ({}) in CubicPolyInterpolator", m_n); } CubicPolyInterpolator::~CubicPolyInterpolator () { } double CubicPolyInterpolator::interpolate (double x) { int lo = static_cast(floor(x)) - 1; int hi = lo + 3; if (lo < -1) { #ifdef DEBUG sys_error (ERR_WARNING, "x={}, out of range [CubicPolyInterpolator]", x); #endif return (0); } else if (lo == -1) // linear interpolate at between x = 0 & 1 return m_pdY[0] + x * (m_pdY[1] - m_pdY[0]); if (hi > m_n) { #ifdef DEBUG sys_error (ERR_WARNING, "x={}, out of range [CubicPolyInterpolator]", x); #endif return (0); } else if (hi == m_n) {// linear interpolate between x = (n-2) and (n-1) double frac = x - (lo + 1); return m_pdY[m_n - 2] + frac * (m_pdY[m_n - 1] - m_pdY[m_n - 2]); } // Lagrange formula for N=4 (cubic) double xd_0 = x - lo; double xd_1 = x - (lo + 1); double xd_2 = x - (lo + 2); double xd_3 = x - (lo + 3); static double oneSixth = (1. / 6.); double y = xd_1 * xd_2 * xd_3 * -oneSixth * m_pdY[lo]; y += xd_0 * xd_2 * xd_3 * 0.5 * m_pdY[lo+1]; y += xd_0 * xd_1 * xd_3 * -0.5 * m_pdY[lo+2]; y += xd_0 * xd_1 * xd_2 * oneSixth * m_pdY[lo+3]; return (y); } CubicSplineInterpolator::CubicSplineInterpolator (const double* const y, const int n) : m_pdY(y), m_n(n) { // Precalculate 2nd derivative of y and put in m_vecY2 // Calculated by solving set of simultaneous CubicSpline spline equations // Only n-2 CubicSpline spline equations, but able to make two more // equations by setting second derivative to 0 at ends m_vecY2.assign (n, 0.); m_vecY2[0] = 0; // second deriviative = 0 at beginning and end m_vecY2[n-1] = 0; std::vector temp (n - 1); temp[0] = 0; int i; for (i = 1; i < n - 1; i++) { double t = 2 + (0.5 * m_vecY2[i-1]); temp[i] = y[i+1] + y[i-1] - y[i] - y[i]; temp[i] = (3 * temp[i] - 0.5 * temp[i-1]) / t; m_vecY2[i] = -0.5 / t; } for (i = n - 2; i >= 0; i--) m_vecY2[i] = temp[i] + m_vecY2[i] * m_vecY2[i + 1]; } CubicSplineInterpolator::~CubicSplineInterpolator () { } double CubicSplineInterpolator::interpolate (double x) { const static double oneSixth = (1. / 6.); int lo = static_cast(floor(x)); int hi = lo + 1; if (lo < 0 || hi >= m_n) { #ifdef DEBUG sys_error (ERR_SEVERE, "x out of bounds [CubicSplineInterpolator::interpolate]"); #endif return (0); } double loFr = hi - x; double hiFr = 1 - loFr; double y = loFr * m_pdY[lo] + hiFr * m_pdY[hi]; y += oneSixth * ((loFr*loFr*loFr - loFr) * m_vecY2[lo] + (hiFr*hiFr*hiFr - hiFr) * m_vecY2[hi]); return y; } ctsim-7.0.0/libctsupport/hashtable.cpp0000644000175000017500000000560715253666241017062 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Hash Table Class ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #include "ct.h" #include #include #include #include "ctsupport.h" #include "pol.h" KeywordCodeEntry::KeywordCodeEntry (std::string_view svKeyword, int iCode) : m_iCode (iCode) { // Kept lowercased, so a lookup need only lowercase what it is given. m_strKeyword.reserve (svKeyword.size()); for (char c : svKeyword) m_strKeyword += static_cast (std::tolower (static_cast (c))); } bool KeywordCodeEntry::matchesKeyword (std::string_view svCompare) const { // The stored keyword is lowercased, so this matches without regard to // case. Note that it is the lowercased copy that must be compared, not // the argument as given. return equalsIgnoringCase (m_strKeyword, svCompare); } // inittable (table) // clear symbol table void KeywordCodeHashTable::initTable () { m_vecBuckets.clear(); m_vecBuckets.resize (HASHSIZE); } // freetable (table) // free all memory allocated to table, then clear table void KeywordCodeHashTable::freeTable () { m_vecBuckets.clear(); initTable (); } // form hash value of string s int KeywordCodeHashTable::hash (std::string_view s) { int hashval = 0; for (char c : s) hashval += std::tolower (static_cast (c)); return (hashval % HASHSIZE); } /* Look for s in hash table */ KeywordCodeEntry * KeywordCodeHashTable::lookup (std::string_view svLookup) { for (const auto& pEntry : m_vecBuckets[hash (svLookup)]) if (pEntry->matchesKeyword (svLookup)) return pEntry.get(); return nullptr; } void KeywordCodeHashTable::installKeywordCode (std::string_view svKeyword, int iCode) { KeywordCodeEntry* pFound = lookup (svKeyword); if (pFound != nullptr) { // already defined pFound->setCode (iCode); return; } auto pEntry = std::make_unique (svKeyword, iCode); m_vecBuckets[hash (pEntry->getKeyword())].push_back (std::move (pEntry)); } ctsim-7.0.0/libctsupport/hooks.cpp0000644000175000017500000000315515253666241016246 0ustar kevinkevin/***************************************************************************** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ // optional plot sink for the signal-processing trace levels, so that the // reconstruction code does not depend on the graphical program. #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "ctsupport_hooks.h" static TracePlotHandler s_tracePlotHandler; void traceSetPlotHandler (TracePlotHandler handler) { s_tracePlotHandler = handler; } void traceClearPlotHandler () { s_tracePlotHandler = TracePlotHandler(); } bool traceHasPlotHandler () { return static_cast(s_tracePlotHandler); } void tracePlot (const std::string& strTitle, std::span x, std::span y) { if (s_tracePlotHandler && ! y.empty()) s_tracePlotHandler (strTitle, x, y); } ctsim-7.0.0/libctsupport/syserror.cpp0000644000175000017500000001336515253666241017017 0ustar kevinkevin/***************************************************************************** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #include #include #include #include #include #include "ct.h" #include "ctsupport_hooks.h" void sysErrorDispatch (int iSeverity, const std::string& strMessage); /* NAME * sys_error System error handler * * SYNOPSIS * sys_error (severity, message) or sys_error (severity, format, args...) * int severity Severity of error * message The text, already formatted * args Argument list, direct transfer to printf stack */ bool equalsIgnoringCase (std::string_view a, std::string_view b) { if (a.size() != b.size()) return false; for (std::size_t i = 0; i < a.size(); i++) { // Cast through unsigned char: tolower on a negative char is undefined, // which a byte above 127 in a file name would give it. const unsigned char ca = static_cast (a[i]); const unsigned char cb = static_cast (b[i]); if (std::tolower (ca) != std::tolower (cb)) return false; } return true; } static int s_reportErrorLevel = ERR_TRACE; // Set error reporting level void sys_error (int severity, std::string_view message) { std::string strOutput; sys_verror (strOutput, severity, message); if (strOutput.empty()) return; // below the reporting level, or past the limit if (sysErrorHasHandler()) sysErrorDispatch (severity, strOutput); else std::cout << strOutput << "\n"; } static unsigned long s_nErrorCount = 0; unsigned long int g_lSysErrorMaxCount = 2000; void sys_verror (std::string& strOutput, int severity, std::string_view message) { if (severity < s_reportErrorLevel) return; // ignore error if less than reporting level std::ostringstream os; if (severity > ERR_TRACE) s_nErrorCount++; if (severity != ERR_FATAL) { if (s_nErrorCount > g_lSysErrorMaxCount) return; else if (s_nErrorCount == g_lSysErrorMaxCount) { os << "*****************************************************************\n"; os << "*** M A X I M U M E R R O R C O U N T R E A C H E D ***\n"; os << "*** ***\n"; os << "*** No further errors will be reported ***\n"; os << "*****************************************************************\n"; strOutput = os.str(); return; } } switch (severity) { case ERR_FATAL: os << "FATAL ERROR: "; break; case ERR_SEVERE: os << "SEVERE ERROR: "; break; case ERR_WARNING: os << "WARNING ERROR: "; break; case ERR_TRACE: os << "Trace: "; break; default: os << "Illegal error severity #" << severity << ": "; } // The message as given: the caller has formatted it already, and // formatting it again through a fixed buffer would truncate a long one. os << message; strOutput = os.str(); if (severity == ERR_FATAL) { std::cerr << strOutput << "\n"; throw std::runtime_error (strOutput); } #if INTERACTIVE_ERROR_DISPLAY std::cout << "A - Abort C - Continue W - Turn off warnings? "; // fflush(stderr); do { int c = cio_kb_waitc("AaBbCcWw", true); /* get code from keyboard */ c = tolower (c); fputc (c, stderr); fputc (NEWLINE, stderr); if (c == 'a') exit (1); else if (c == 'c') return; else if (c == 'w') { sys_error_level (ERR_SEVERE); /* report severe & fatal errors */ break; } } while (true); #endif } /* NAME * sys_error_level Set error reporting level * * SYNOPSIS * sys_error_level (severity) * int severity Report all error as serious as severity and beyond * * DESCRIPTION * Causes the system to ignore all error below the level of severity * For example, if severity == ERR_SEVERE, then report severe & fatal * error and ignore warnings */ void sys_error_level (int severity) { if (severity == ERR_FATAL || severity == ERR_SEVERE || severity == ERR_WARNING || severity == ERR_TRACE) s_reportErrorLevel = severity; else s_reportErrorLevel = ERR_WARNING; } // --------------------------------------------------------------------------- // Diagnostic sink (6.4.0). See ctsupport_hooks.h. // --------------------------------------------------------------------------- static SysErrorHandler s_sysErrorHandler; SysErrorHandler sysErrorGetHandler () { return s_sysErrorHandler; } void sysErrorSetHandler (SysErrorHandler handler) { s_sysErrorHandler = handler; } void sysErrorClearHandler () { s_sysErrorHandler = SysErrorHandler(); } bool sysErrorHasHandler () { return static_cast(s_sysErrorHandler); } void sysErrorDispatch (int iSeverity, const std::string& strMessage) { if (s_sysErrorHandler) s_sysErrorHandler (iSeverity, strMessage); } ctsim-7.0.0/libctsupport/clip.cpp0000644000175000017500000002477515253543770016066 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: clip.c ** Purpose: Line clipping routines ** Programmer: Kevin Rosenberg ** Date Started: 1984 ** ** OVERVIEW ** Routines to clip lines against objects ** All routines get the endpoints of the line, and ** the SNARK size of the object (u,v) ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #include "ctsupport.h" /* NAME * clip_segment Clip against a segment of a circle * * SYNOPSIS * clip_segment (x1, y1, x2, y2, u, v) * double& x1,*y1,*x2,*y2 Endpoints of line * double u,v Dimensions of segment */ bool clip_segment (double& x1, double& y1, double& x2, double& y2, const double u, const double v) { double xc1 = x1 * u; double yc1 = y1 * v; double xc2 = x2 * u; double yc2 = y2 * v; if (yc1 > 0. && yc2 > 0.) // reject lines above y-axis return false; double radius = sqrt (u * u + v * v); if (clip_circle (xc1, yc1, xc2, yc2, 0.0, v, radius, 0.0, 0.0) == false) return false; if (yc1 > 0. && yc2 > 0.) // trivial reject above y-axis return false; // clip above x-axis if (yc1 > 0.) { xc1 = xc1 + (xc2-xc1)*(0.0-yc1)/(yc2-yc1); yc1 = 0.0; } else if (yc2 > 0.) { xc2 = xc1 + (xc2-xc1)*(0.0-yc1)/(yc2-yc1); yc2 = 0.0; } x1 = xc1 / u; y1 = yc1 / v; x2 = xc2 / u; y2 = yc2 / v; return true; } /* NAME * clip_sector Clip a line against a sector of a circle * * SYNOPSIS * clip_sector (x1, y1, x2, y2, u, v) * double& x1,*y1,*x2,*y2 Endpoints of line * double u,v Size of sector */ bool clip_sector (double& x1, double& y1, double& x2, double& y2, const double u, const double v) { double xc1 = x1 * u; double yc1 = y1 * v; double xc2 = x2 * u; double yc2 = y2 * v; double radius = sqrt (u * u + v * v); if (clip_circle (xc1, yc1, xc2, yc2, 0.0, v, radius, 0.0, 0.0) == false) return false; if (clip_triangle (xc1, yc1, xc2, yc2, u, v, false) == false) return false; x1 = xc1 / u; y1 = yc1 / v; x2 = xc2 / u; y2 = yc2 / v; return true; } /* NAME * clip_circle Clip a line against a circle * * SYNOPSIS * clip_circle (x1,y1,x2,y2,cx,cy,radius,t1,t2) * double& x1,*y1,*x2,*y2 Endpoints of line to be clipped * double cx,cy Center of circle * double radius Radius of circle * double t1,t2 Starting & stopping angles of clipping */ bool clip_circle (double& x1, double& y1, double& x2, double& y2, const double cx, const double cy, const double radius, double t1, double t2) { double xc1 = x1; double yc1 = y1; double xc2 = x2; double yc2 = y2; double ccx = cx; double ccy = cy; double xtrans = -xc1; // move (x1,y1) to origin double ytrans = -yc1; xc1 += xtrans; yc1 += ytrans; xc2 += xtrans; yc2 += ytrans; ccx += xtrans; ccy += ytrans; double theta = -atan2 (yc2, xc2); // rotate line to lie on x-axis GRFMTX_2D rotmtx; rot_mtx2 (rotmtx, theta); // xc1,yc1 is at origin, no need to rot xform_mtx2 (rotmtx, xc2, yc2); xform_mtx2 (rotmtx, ccx, ccy); t1 += theta; // rotate start and stop angles t2 += theta; t1 = normalizeAngle (t1); t2 = normalizeAngle (t2); if (xc2 < -D_EPSILON || fabs(yc2) > F_EPSILON) { sys_error (ERR_SEVERE, "Internal error in clip_circle\n x1={:6.2f}, y1={:6.2f}, x2={:6.2f}, y2={:6.2f}, xc2={:6.2f}, yc2={:6.2f}, theta={:6.2f}", x1, y1, x2, y2, xc2, yc2, theta); return false; } if (fabs(ccy) > radius) /* check if can reject */ return false; double temp = sqrt (radius * radius - ccy * ccy); double xcmin = ccx - temp; double xcmax = ccx + temp; if (fabs(t2 - t1) < D_EPSILON) { if (xc1 < xcmin) xc1 = xcmin; if (xc2 > xcmax) xc2 = xcmax; } else if (t1 < t2) { if (t1 < PI && t2 > PI) if (xc1 < xcmin) xc1 = xcmin; } else if (t1 > t2) { if (t1 < PI) if (xc1 < xcmin) xc1 = xcmin; if (xc2 > xcmax) xc2 = xcmax; } rot_mtx2 (rotmtx, -theta); xform_mtx2 (rotmtx, xc1, yc1); xform_mtx2 (rotmtx, xc2, yc2); xc1 += -xtrans; yc1 += -ytrans; xc2 += -xtrans; yc2 += -ytrans; x1 = xc1; y1 = yc1; x2 = xc2; y2 = yc2; return true; } /* NAME * clip_triangle Clip a line against a triangle * * SYNOPSIS * clip_triangle (x1, y1, x2, y2, u, v, clip_xaxis) * double& x1, *y1, *x2, *y2 Endpoints of line * double u, v Size of 1/2 base len & height * int clip_xaxis Boolean flag whether to clip against x axis * (Use true for all triangles) * (false if used internally by sector clipping routine) * * DESCRIPTION * x * /|\ Note that vertices of triangle are * / | \ (-u, 0) * / | \ (u, 0) * / | \ (0, v) * / | v \ * / | \ * +------+------+ * (0,0) u * * NOTES * 1) Inside of this routine, values of (u,v) are assumed to be (1,1) * * 2) Derivation of clipping equations: * Using parametric equations for the line * xv = x1 + t * (x2 - x1) * yv = y1 + t * (y2 - y1) * so, * t = (xv - x1) / (x2 - x1) * yv = y1 + (xv - x1) * (y2 - y1) / (x2 - x1) * yv = y1 + (xv - x1) * dy / dx * * Now, find the intersections with the following clipping boundries: * yv = v - (v/u) * xv (yv = mx + b) * yv = v + (v/u) * xv (m = v/u, b = v); */ static int tcode (const double x, const double y, const double m, const double b, const int clip_xaxis); bool clip_triangle (double& x1, double& y1, double& x2, double& y2, const double u, const double v, const int clip_xaxis) { double m = v / u; // slope of triangle lines double b = v; // y-intercept of triangle lines int c1 = tcode (x1, y1, m, b, clip_xaxis); int c2 = tcode (x2, y2, m, b, clip_xaxis); #if 0 printf ("x1:%6.2f y1:%6.2f code1:%2d x2:%6.2f y2:%6.2f code2:%2d\n", x1, y1, c1, x2, y2, c2); #endif while ( c1 || c2 ) { if ( c1 & c2 ) { return false; // trivial reject } int c = c1; if (c1 == 0) c = c2; double x = 0, y = 0; if (c & 1) { // below x = x1 + (x2-x1)*(0.0-y1)/(y2-y1); y = 0.0; } else if (c & 2) { // right double dx, dy; dx = x2 - x1; dy = y2 - y1; if (fabs(dx) > D_EPSILON) x = (-y1 + b + x1 * dy/dx) / (m + dy/dx); else x = x1; y = -m * x + b; } else if (c & 4) { /* left */ double dx, dy; dx = x2 - x1; dy = y2 - y1; if (fabs(dx) > D_EPSILON) { x = (y1 - b - x1 * dy/dx); x /= (m - dy/dx); } else x = x1; y = m * x + b; } if (c == c1) { x1=x; y1=y; c1=tcode (x1,y1,m,b,clip_xaxis); } else { x2=x; y2=y; c2=tcode (x2,y2,m,b,clip_xaxis); } #if 0 printf ("x1:%6.2f y1:%6.2f code1:%2d x2:%6.2f y2:%6.2f code2:%2d\n", x1, y1, c1, x2, y2, c2); #endif } return true; /* we have clipped the line, and it is good */ } /* compute region code */ static int tcode (const double x, const double y, const double m, const double b, const int clip_xaxis) { int c = 0; if (clip_xaxis && y < 0.) // below triange c = 1; if (y > -m * x + b + D_EPSILON) // right of triangle c += 2; if (y > m * x + b + D_EPSILON) // left of triangle c += 4; return (c); } /* NAME * clip_rect Clip a line against a rectangle * * SYNOPSIS * clip_rect (x1, y1, x2, y2, rect) * double& x1, *y1, *x2, *y2 Endpoints of line * double rect[4] Rectangle to clip against * ordered xmin, ymin, xmax, ymax */ static int rectcode (double x, double y, const double rect[4]); bool clip_rect (double& x1, double& y1, double& x2, double& y2, const double rect[4]) { double x = 0, y = 0; int c1 = rectcode (x1, y1, rect); int c2 = rectcode (x2, y2, rect); while (c1 || c2) { if (c1 & c2) return false; // trivial reject int c = c1; if (c1 == 0) c = c2; if (c & 1) { // left y = y1 + (y2-y1)*(rect[0]-x1)/(x2-x1); x = rect[0]; } else if (c & 2) { // right y = y1 + (y2-y1)*(rect[2]-x1)/(x2-x1); x = rect[2]; } else if (c & 4) { // bottom x = x1 + (x2-x1)*(rect[1]-y1)/(y2-y1); y = rect[1]; } else if (c & 8) { // top x = x1 + (x2-x1)*(rect[3]-y1)/(y2-y1); y = rect[3]; } if (c == c1) { x1=x; y1=y; c1=rectcode(x1,y1,rect); } else { x2=x; y2=y; c2=rectcode(x2,y2,rect); } } return true; // we have clipped the line, and it is good } /* NAME * rectcode INTERNAL routine to return position of * point relative to a rectangle * * SYNOPSIS * c = rectcode (x, y, rect) * int c Position of point relative to the window * double x, y Point to test against window * double rect[4] Coordinates of rectangle extent * Ordered [xmin, ymin, xmax, ymax] */ static int rectcode (double x, double y, const double rect[4]) { int c = 0; if (x < rect[0]) c = 1; else if (x > rect[2]) c = 2; if (y < rect[1]) c += 4; else if (y > rect[3]) c += 8; return (c); } ctsim-7.0.0/libctsupport/timeformat.cpp0000644000175000017500000000323115253543770017266 0ustar kevinkevin/***************************************************************************** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "timeformat.h" #include std::string formatCalcTimeStd (double dSeconds) { if (! (dSeconds > 0.)) return "0 s"; char szBuf[64]; if (dSeconds >= 60.) { const int iMinutes = static_cast (dSeconds / 60.); snprintf (szBuf, sizeof(szBuf), "%d min %.3f s", iMinutes, dSeconds - iMinutes * 60.); } else if (dSeconds >= 1.) snprintf (szBuf, sizeof(szBuf), "%.3f s", dSeconds); else if (dSeconds >= 1e-3) snprintf (szBuf, sizeof(szBuf), "%.3f ms", dSeconds * 1e3); else if (dSeconds >= 1e-6) snprintf (szBuf, sizeof(szBuf), "%.3f us", dSeconds * 1e6); else snprintf (szBuf, sizeof(szBuf), "%.3f ns", dSeconds * 1e9); return szBuf; } ctsim-7.0.0/libctsupport/globalvars.cpp0000644000175000017500000000225615253666241017260 0ustar kevinkevin/***************************************************************************** ** FILE IDENTIFICATION ** ** Name: globalvars.cpp ** Purpose: Global variables ** Programmer: Kevin Rosenberg ** Date Started: Jan 2001 ** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #include "ct.h" // The GUI's globals are not here; see ctsupport_hooks.h. ctsim-7.0.0/libctsupport/consoleio.cpp0000644000175000017500000001032215253543770017110 0ustar kevinkevin/***************************************************************************** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #include #include #include "ctsupport.h" /* NAME * cio_put_c Put a character on screen * * SYNOPSIS * cio_put_c (c) * char c Character to write * * NOTES * Color of character is determined by the global variable, crtv.text_attr. * * SIDE EFFECTS * Cursor is advanced by one. If necessary, the cursor will wrap around * and maybe the screen will scroll */ void cio_put_c (int c) { fputc(c, stdout); } /* NAME * cio_put_cc Put a char on screen count times * * SYNOPSIS * cio_put_cc (c, count) * char c Character to write * int count Number of characters to write */ void cio_put_cc (int c, int count) { for (int i = 0; i < count; i++) cio_put_c (c); } void cio_put_str (const char *str) { fputs (str, stdout); } /* NAME * kb_getc Get a character from the keyboard * * SYNOPSIS * key = kb_getc() * * DESCRIPTION * 1. This routine returns an EXTENTED ASCII code, * the extended codes have a low byte of 0 and a distinctive * high byte, such as 0x2D00 and 0x3200 * 2. This routine waits until a key has been typed * 2. The keystroke will not be echoed. */ unsigned int cio_kb_getc(void) { return fgetc(stdin); } void cio_kb_ungetc (unsigned int c) { ungetc(c, stdin); } /* NAME * kb_gets Get a string from the keyboard * * SYNOPSIS * str = kb_gets (str, maxlen) * char *str Space to store input string * int maxlen Maximum number of characters to read * (Not including EOS) * NOTES * Backspace - erases character to the right * Escape - erases to beginning of line * Return - ends string (no not cause a linefeed) */ char * cio_kb_gets (char *str, int maxlen) { return fgets(str, maxlen, stdin); } /* NAME * kb_waitc Wait for a character from the keyboard * * SYNOPSIS * key = kb_waitc (astr, estr, beep) * int key Keystroke entered * char *astr String of valid ascii characters * bool beep If true, beep when user hits invalid key * */ unsigned int cio_kb_waitc (const char *astr, int beep_on_error) { unsigned int c; do { c = cio_kb_getc (); if (strchr (astr, c) != nullptr) break; if (beep_on_error) cio_beep(); } while (1); return (c); } /* NAME * beep sound a beep to user * * SYNOPSIS * beep() */ void cio_beep (void) { cio_tone (2000.0, 0.3); } /* NAME * tone play a frequency sound for some duration * * SYNOPSIS * tone (freq, length) * double freq frequency to play in Hertz * double length duration to play note in seconds */ void cio_tone (double freq, double length) { #if 1 fprintf(stdout, "\007"); #else cio_spkr_freq (freq); /* Set frequency of tone */ cio_spkr_on (); /* Turn on speaker */ pause (length); /* Pause for length seconds */ cio_spkr_off (); /* Turn off speaker */ #endif } ctsim-7.0.0/libctsupport/mathfuncs.cpp0000644000175000017500000000777515253666241017127 0ustar kevinkevin/***************************************************************************** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #include "ctsupport.h" /* NAME * integrateSimpson Integrate array of data by Simpson's rule * * SYNOPSIS * double integrateSimpson (xmin, xmax, y, np) * double xmin, xmax Extent of integration * double y[] Function values to be integrated * int np number of data points * (must be an odd number and at least 3) * * RETURNS * integrand of function */ double integrateSimpson (const double xmin, const double xmax, const double *y, const int np) { if (np < 2) return (0.); else if (np == 2) return ((xmax - xmin) * (y[0] + y[1]) / 2); double area = 0; int nDiv = (np - 1) / 2; // number of divisions double width = (xmax - xmin) / static_cast (np - 1); // width of cells for (int i = 1; i <= nDiv; i++) { int xr = 2 * i; int xl = xr - 2; // 2 * (i - 1) == 2 * i - 2 == xr - 2 int xm = xr - 1; // (xl+xr)/2 == (xr+xr-2)/2 == (2*xr-2)/2 = xr-1 area += (width / 3.0) * (y[xl] + 4.0 * y[xm] + y[xr]); } if ((np & 1) == 0) /* do last trapazoid */ area += width * (y[np-2] + y[np-1]) / 2; return (area); } /* NAME * normalizeAngle Normalize angle to 0 to 2 * PI range * * SYNOPSIS * t = normalizeAngle (theta) * double t Normalized angle * double theta Input angle */ double normalizeAngle (double theta) { while (theta < 0.) theta += TWOPI; while (theta >= TWOPI) theta -= TWOPI; return (theta); } void vectorNumericStatistics (std::vector vec, const int nPoints, double& min, double& max, double& mean, double& mode, double& median, double& stddev) { if (nPoints <= 0) return; mean = 0; min = vec[0]; max = vec[0]; int i; for (i = 0; i < nPoints; i++) { double v = vec[i]; if (v > max) max = v; if (v < min) min = v; mean += v; } mean /= nPoints; static const int nbin = 1024; int hist[ nbin ] = {0}; double spread = max - min; mode = 0; stddev = 0; // guard the bin index. A constant image gives spread == 0, and a // non-finite pixel gives a NaN ratio; either way the cast to int is // undefined and would index far outside hist[]. for (i = 0; i < nPoints; i++) { double v = vec[i]; int b = 0; if (spread > 0.) { double dRatio = (v - min) / spread; if (dRatio >= 0. && dRatio <= 1.) // false for NaN, as intended b = static_cast((dRatio * (nbin - 1)) + 0.5); if (b < 0) b = 0; else if (b >= nbin) b = nbin - 1; } hist[b]++; double diff = (v - mean); stddev += diff * diff; } stddev = sqrt (stddev / nPoints); int max_binindex = 0; int max_bin = -1; for (int ibin = 0; ibin < nbin; ibin++) { if (hist[ibin] > max_bin) { max_bin = hist[ibin]; max_binindex = ibin; } } mode = (max_binindex * spread / (nbin - 1)) + min; std::sort(vec.begin(), vec.end()); if (nPoints % 2) // Odd median = vec[((nPoints - 1) / 2)]; else // Even median = (vec[ (nPoints / 2) - 1 ] + vec[ nPoints / 2 ]) / 2; } ctsim-7.0.0/libctsupport/xform.cpp0000644000175000017500000000726715253543770016267 0ustar kevinkevin/***************************************************************************** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #include "ctsupport.h" /* NAME * rotate2d Rotates an array of 2 dimensional vectors * * SYNOPSIS * rotate2d (x, y, n, angle) * double x[], y[] Array of points * int n Number of points in array * double angle Rotation angle (counter-clockwise) */ void rotate2d (double x[], double y[], int n, double angle) { double cos_theta = cos (angle); double sin_theta = sin (angle); for (int i = 0; i < n; i++) { double xrot = x[i] * cos_theta - y[i] * sin_theta; double yrot = x[i] * sin_theta + y[i] * cos_theta; x[i] = xrot; y[i] = yrot; } } /* NAME * xlat2d Translates an array of 2 dimensional vectors * * SYNOPSIS * xlat2d (x, y, n, xoffset, yoffset) * double x[], y[] Array of points * int n Number of points in array * double xoffset, yoffset Offset to translate points by */ void xlat2d (double x[], double y[], int n, double xoffset, double yoffset) { for (int i = 0; i < n; i++) { x[i] += xoffset; y[i] += yoffset; } } /* NAME * scale2d Scale an array of 2 dimensional vectors * * SYNOPSIS * scale2d (x, y, n, xoffset, yoffset) * double x[], y[] Array of points * int n Number of points in array * double xfact, yfact x & y scaling factors */ void scale2d (double x[], double y[], int n, double xfact, double yfact) { for (int i = 0; i < n; i++) { x[i] *= xfact; y[i] *= yfact; } } void indent_mtx2 (GRFMTX_2D m) { m[0][0] = 1.0; m[0][1] = 0.0; m[0][2] = 0.0; m[1][0] = 0.0; m[1][1] = 1.0; m[1][2] = 0.0; m[2][0] = 0.0; m[2][1] = 0.0; m[2][2] = 1.0; } void xlat_mtx2 (GRFMTX_2D m, const double x, const double y) { indent_mtx2 (m); m[2][0] = x; m[2][1] = y; } void scale_mtx2 (GRFMTX_2D m, const double sx, const double sy) { indent_mtx2 (m); m[0][0] = sx; m[1][1] = sy; } void rot_mtx2 (GRFMTX_2D m, const double theta) { double c = cos(theta); double s = sin(theta); indent_mtx2 (m); m[0][0] = c; m[0][1] = s; m[1][0] = -s; m[1][1] = c; } void copy_mtx2 (GRFMTX_2D to, const GRFMTX_2D from) { for (int r = 0; r < 3; r++) for (int c = 0; c < 3; c++) to[r][c] = from[r][c]; } void mult_mtx2 (const GRFMTX_2D m1, const GRFMTX_2D m2, GRFMTX_2D result) { GRFMTX_2D temp; for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) { temp[row][col] = 0; for (int calc = 0; calc < 3; calc++) temp[row][col] += m1[row][calc] * m2[calc][col]; } } copy_mtx2 (result, temp); } void xform_mtx2 (const GRFMTX_2D m, double& x, double& y) { double xt = x * m[0][0] + y * m[1][0] + m[2][0]; double yt = x * m[0][1] + y * m[1][1] + m[2][1]; x = xt; y = yt; } ctsim-7.0.0/libctsupport/fnetorderstream.cpp0000644000175000017500000000777615253543770020345 0ustar kevinkevin/***************************************************************************** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ #if HAVE_CONFIG_H #include "config.h" #endif #include "ctsupport.h" #include "fnetorderstream.h" void ConvertNetworkOrder (void* buffer, size_t bytes) { #ifndef WORDS_BIGENDIAN if (bytes < 2) return; char* start = static_cast(buffer); char* end = start + bytes - 1; // last byte size_t nSwap = bytes / 2; while (nSwap-- > 0) { unsigned char c = *start; *start++ = *end; *end-- = c; } #endif } void ConvertReverseNetworkOrder (void* buffer, size_t bytes) { #ifdef WORDS_BIGENDIAN if (bytes < 2) return; char* start = static_cast(buffer); char* end = start + bytes - 1; // last byte size_t nSwap = bytes / 2; while (nSwap-- > 0) { unsigned char c = *start; *start++ = *end; *end-- = c; } #endif } void fnetorderstream::writeInt16 (kuint16 n) { #ifndef WORDS_BIGENDIAN SwapBytes2 (&n); #endif write (reinterpret_cast(&n), 2); } void fnetorderstream::writeInt32 (kuint32 n) { #ifndef WORDS_BIGENDIAN SwapBytes4(&n); #endif write (reinterpret_cast(&n), 4); } void fnetorderstream::writeFloat32 (kfloat32 n) { #ifndef WORDS_BIGENDIAN SwapBytes4 (&n); #endif write (reinterpret_cast(&n), 4); } void fnetorderstream::writeFloat64 (kfloat64 n) { #ifndef WORDS_BIGENDIAN SwapBytes8 (&n); #endif write (reinterpret_cast(&n), 8); } void fnetorderstream::readInt16 (kuint16& n) { read (reinterpret_cast(&n), 2); #ifndef WORDS_BIGENDIAN SwapBytes2 (&n); #endif } void fnetorderstream::readInt32 (kuint32& n) { read (reinterpret_cast(&n), 4); #ifndef WORDS_BIGENDIAN SwapBytes4 (&n); #endif } void fnetorderstream::readFloat32 (kfloat32& n) { read (reinterpret_cast(&n), 4); #ifndef WORDS_BIGENDIAN SwapBytes4 (&n); #endif } void fnetorderstream::readFloat64 (kfloat64& n) { read (reinterpret_cast(&n), 8); #ifndef WORDS_BIGENDIAN SwapBytes8 (&n); #endif } void frnetorderstream::writeInt16 (kuint16 n) { #ifdef WORDS_BIGENDIAN SwapBytes2 (&n); #endif write (reinterpret_cast(&n), 2); } void frnetorderstream::writeInt32 (kuint32 n) { #ifdef WORDS_BIGENDIAN SwapBytes4(&n); #endif write (reinterpret_cast(&n), 4); } void frnetorderstream::writeFloat32 (kfloat32 n) { #ifdef WORDS_BIGENDIAN SwapBytes4 (&n); #endif write (reinterpret_cast(&n), 4); } void frnetorderstream::writeFloat64 (kfloat64 n) { #ifdef WORDS_BIGENDIAN SwapBytes8 (&n); #endif write (reinterpret_cast(&n), 8); } void frnetorderstream::readInt16 (kuint16& n) { read (reinterpret_cast(&n), 2); #ifdef WORDS_BIGENDIAN SwapBytes2 (&n); #endif } void frnetorderstream::readInt32 (kuint32& n) { read (reinterpret_cast(&n), 4); #ifdef WORDS_BIGENDIAN SwapBytes4 (&n); #endif } void frnetorderstream::readFloat32 (kfloat32& n) { read (reinterpret_cast(&n), 4); #ifdef WORDS_BIGENDIAN SwapBytes4 (&n); #endif } void frnetorderstream::readFloat64 (kfloat64& n) { read (reinterpret_cast(&n), 8); #ifdef WORDS_BIGENDIAN SwapBytes8 (&n); #endif } ctsim-7.0.0/NEWS0000644000175000017500000001153315253543770012364 0ustar kevinkevinCTSim 7.0.0 is the first release since 6.0.2 in 2010. The graphical interface is ported from wxWidgets to Qt 6, and CTSim now builds on Windows and macOS as well as Linux. One program is built twice: ctsim with the interface, ctsim-nogui without it; both run the same tools, named on the command line or typed at a prompt. Scripts drive the whole program and record what they did. Files can be HDF5, and images can be imported from and exported to DICOM. The build is CMake. The number of detectors, the samples per pixel and the size of a reconstructed image must now be odd. See ChangeLog. CTSim 6.5.0 adds the HDF5 file format with structured history, figure conversion for the manual, man pages and line editing in the shell. See ChangeLog. CTSim 6.4.0 adds DICOM import and export, and direct Fourier reconstruction by gridding. See ChangeLog. CTSim 6.3.0 decouples the library from the interface and adds the unit test suite. See ChangeLog. CTSim 6.2.1 corrects a heap overflow in the FFTW wisdom path found by AddressSanitizer. See ChangeLog. CTSim 6.2.0 replaces autotools with CMake, builds warning-free under -Wall -Wextra, and runs clean under AddressSanitizer/UBSan. See ChangeLog. CTSim 6.1.0 adds PNG raster phantom elements, requires C++20, relicenses to GPL-3.0-or-later, and converts image storage to std::vector. Built on the 6.0.3 git tree, preserving its OpenMP work. See ChangeLog. CTSim 6.0.3 adds the test suite in tests/, more OpenMP multithreading and several loop optimizations. See ChangeLog. CTSim 6.0.2 was the last release of the wxWidgets line, in March 2018. It completed the OpenMP work begun in 6.0.0, added a script comparing the results of OpenMP and non-OpenMP builds, and cleared the remaining compiler warnings. 6.0.0, a day earlier, migrated to wxGTK 3.0 and made OpenMP parallelism the default. See ChangeLog. CTSim 5.2.1 (2012) and 5.2.0 (2010) were small: a text output option for ifexport, contributed by Christian Thibaudeau, and a spelling fix. The 5.1 series (2009) finished the wxWidgets 2.8 port begun in 5.0, fixing the known compatibility problems, the fonts and the numbering of unnamed documents. 5.0.0 (2009) was the port itself, a major revision of the wxWidgets code from 2.4 to 2.8. See ChangeLog. Version 4.2 Released Mar 2003 * Direct Fourier Reconstructions now supported Version 4.0 Released Jan 2003 * Port to wxWindows 2.4 * Port to Mac OS X Version 3.5 Released May 2002 * New reconstruction technique: Interpolate divergent beams to parallel beams for faster reconstructions. * Multithreading for running length processes in background. Also takes advantage of multiple-CPU's in SMP systems for speed improvements with parallel processing. * Region of interest can be specified for reconstruction zooming. * Import PNG, PPM, and PGM images * DICOM file import/export * Clipboard for cutting and pasting images * Can read and reconstruct SIEMENS AR.STAR DICOM raw data files * Scattergram of T-Theta sampling. Useful for understanding divergent beam scanning. * Startup-tips to help new users learn features. * Ability to place detector at any arbitray position * Histogram plotting of projection files Version 3.0 New Features * Greatly improved dialog boxes * Online and print manuals with context-sensitive help! * 3-d rotating views of image files * Creation of arbitrary filter images * Cubic interpolation for reconstructions * All features of command line tools are now in graphical ctsim program! * Complex-valued image files now supported, lots of image math functions added including Fourier transformations * Visual and statistical image comparision functions * Plotting of row and column data of single and comparison images * Histogram Plotting of Image files. * Conversion of projections to polar images * Improved animation graphics * New Microsoft Windows compatible self-installer with explorer extensions to CTSim files Version 2.5 of CTSim has been released! New Features of CTSim version 2.5: - Now compiles with Microsoft Visual C++. - First Microsoft Windows graphical user interface - Bug fixes and modest documentation and program improvements over version 2.0 New Features of CTSim version 2.0: - Entire code-base cleaned up, re-written, and converted from C to C++. - Graphical user interface now included! Animation of projection and reconstruction processes. Displays projection and image files. Uses platform-independent wxWindows library so can run on UNIX and Microsoft Windows platforms. - Integrated G2 library for graphical display from command line tools (UNIX only) - Added frequency-based filtering. Can provide significant speed-up in reconstructions compared to convolution-based filtering. - Added highly-optimized backprojection method (idiff3) - New command-line tools added for image statistics and comparisons with both text and graphical outputs. ctsim-7.0.0/COPYRIGHT0000644000175000017500000000166315253543770013163 0ustar kevinkevin/***************************************************************************** ** This is part of the CTSim program ** Copyright (c) 1983-2026 Kevin Rosenberg ** SPDX-License-Identifier: GPL-3.0-or-later ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ******************************************************************************/ ctsim-7.0.0/scripts/0000755000175000017500000000000015253702131013335 5ustar kevinkevinctsim-7.0.0/scripts/check-includes.py0000755000175000017500000000535315253543770016615 0ustar kevinkevin#!/usr/bin/env python3 """Check that a header includes the standard header for what it uses. libstdc++ includes a great deal transitively: a header that says std::span without including compiles on Linux and fails on macOS, where libc++ includes almost nothing it is not asked to. Every such fault found so far was invisible until someone built on a Mac. Run from the top of the source tree: python3 scripts/check-includes.py """ import pathlib import re import sys # What a use requires. Only the ones this tree uses; add as needed. NEED = { 'std::span': '', 'std::string_view': '', 'std::vector': '', 'std::unique_ptr': '', 'std::shared_ptr': '', 'std::int64_t': '', 'std::uint64_t': '', 'std::int32_t': '', 'std::uint32_t': '', 'std::int16_t': '', 'std::uint16_t': '', 'std::int8_t': '', 'std::uint8_t': '', 'std::string': '', 'std::numeric_limits': '', 'std::filesystem': '', 'std::complex': '', 'std::function': '', 'std::map': '', 'std::optional': '', 'std::format': '', } # A use inside a comment is not a use. COMMENT = re.compile(r'//[^\n]*|/\*.*?\*/', re.S) def first_use(text, symbol): """Where a symbol is first used, ignoring comments; None if never.""" stripped = COMMENT.sub(lambda m: ' ' * len(m.group(0)), text) m = re.search(re.escape(symbol) + r'\b', stripped) return m.start() if m else None def check(path): text = path.read_text(errors='replace') problems = [] for symbol, header in NEED.items(): used = first_use(text, symbol) if used is None: continue included = text.find('#include ' + header) if included < 0: problems.append(f'uses {symbol} but never includes {header}') elif included > used: problems.append(f'includes {header} after the first use of {symbol}') return problems def main(): root = pathlib.Path('.') bad = 0 for path in sorted(root.glob('include/*.h')): for problem in check(path): print(f'{path}: {problem}') bad += 1 if bad: print(f'\n{bad} header(s) rely on another header having included ' f'what they use.\nThat works with libstdc++ and fails with ' f'libc++, which is to say it fails on macOS.') return 1 print('Every header includes what it uses.') return 0 if __name__ == '__main__': sys.exit(main()) ctsim-7.0.0/scripts/helical/0000755000175000017500000000000015253770310014742 5ustar kevinkevinctsim-7.0.0/scripts/helical/sample-helical.sh0000755000175000017500000000241615253702131020160 0ustar kevinkevin#!/bin/bash -e if test "$1" != "" ; then bin=$1/ else bin=../../b/ fi # Generate phantom image cc -o dynphm dynphm.c -lm ./dynphm 100 200 tmpphm ${bin}ctsim-nogui phm2if sample-phm.if 255 255 --nsample 3 --phmfile tmpphm rm -f tmpphm if [ -f sample-phm.if ] ; then ${bin}ctsim-nogui ifexport sample-phm.if sample-phm.png --format png fi # Simulate helical CT data collection and generate raysum sinogram for display ${bin}ctsim-nogui phm2helix sample-pj.pj 367 1080 ./dynphm --nray 3 --geometry equiangular --rotangle 3 if [ -f sample-pj.pj ]; then ${bin}ctsim-nogui pj2if sample-pj.pj sample-pj.if fi if [ -f sample-pj.if ]; then ${bin}ctsim-nogui ifexport sample-pj.if sample-pj.png --format png fi if [ -f sample-pj.pj ]; then ${bin}ctsim-nogui pjHinterp sample-pj.pj 540-pj.pj --interpview 540 fi if [ -f 540-pj.pj ]; then ${bin}ctsim-nogui pj2if 540-pj.pj 540-pj.if fi if [ -f sample-pj.if ]; then ${bin}ctsim-nogui ifexport 540-pj.if 540-pj.png --format png fi # Reconstruct raysums and generate image for display ${bin}ctsim-nogui pjrec 540-pj.pj 540-rec.if 255 255 if [ -f sample-rec.if ]; then ${bin}ctsim-nogui ifexport 540-rec.if 540-rec.png --format png # Display comparison statistics ${bin}ctsim-nogui if2 sample-phm.if 540-rec.if --comp fi ctsim-7.0.0/scripts/helical/dynphm.c0000644000175000017500000000171515253702131016405 0ustar kevinkevin#include #include #include int main(int argc, char *argv[]) { char filename[256]; double mu1=0. , mu2=6., density, afac, s; FILE *fp = (FILE *)NULL; if (argc !=4){ fprintf(stderr, "Usage: %s iview nview phmfilename\n", argv[0]); exit (1); } int view = atoi(argv[1]); int nview = atoi(argv[2]); snprintf(filename, sizeof(filename), "%s", argv[3]); s = (double) view / ((double)(nview-1)); if ( s < 7./16. ) density = mu1; else if ( s > 9./16. ) density = mu2; else { afac = ( (s - 7./16.) / 2./16.); density = log(1/((1-afac)*exp(-mu1) + afac * exp(-mu2))); } if ( (fp = fopen(filename, "w")) == (FILE *)NULL){ fprintf(stderr,"Error, can not open file \"tmpphmfile\"\n"); exit(2); } fprintf(fp, "rectangle 0 0 11.5 11.5 0 0\n"); fprintf(fp, "ellipse 0 0 11.4 11.4 0 1\n"); fprintf(fp, "ellipse 0 0 1.25 1.25 0 %f\n", density); fclose(fp); exit(0); } ctsim-7.0.0/scripts/helical/Makefile0000644000175000017500000000014315253702131016374 0ustar kevinkevin.PHONY: clean run run: bash -e ./sample-helical.sh ../../b clean: rm -f dynphm *.if *.pj *.png ctsim-7.0.0/scripts/README.md0000644000175000017500000001421515253543770014633 0ustar kevinkevin# Build scripts Convenience wrappers around the CMake commands in `INSTALL`. Nothing here is required: everything they do can be typed by hand, and the scripts print the commands they run. scripts/build.sh --help Linux and macOS (bash or zsh) scripts\build.bat --help Windows (cmd, with the MSYS2 UCRT64 or Visual Studio tools on the PATH) Both configure, build, test and package from the source tree above them, and both leave that tree alone: build output goes to `b/` (or `$BUILDDIR`), and the Debian packages are built in a copy of the tree under it rather than in place. `--init` installs the dependencies: apt or Homebrew on Linux and macOS, pacman on Windows, where it needs MSYS2. Under Visual Studio the dependencies come from vcpkg and `--init` says so rather than guessing. On Windows the installer carries the libraries the program was linked against, resolved from the executables themselves: the compiler runtime, FFTW, HDF5, DCMTK, libpng and the rest, alongside what windeployqt brings for Qt. They are taken from the compiler's own bin directory, which under MSYS2 is where every package puts its DLLs -- so build and package in the same MSYS2 environment. `--deb` builds the Debian packages and then checks them with lintian, which reads the packaging the way the archive does. Where lintian is not installed the check is skipped and said to be skipped; the packages are built either way, and a tag lintian dislikes does not stop the script. `ctdev.sh` writes shell functions for working on CTSim; evaluate it from `~/.bashrc` or `~/.zshrc`: eval "$(scripts/ctdev.sh)" It defines `ctbuild`, `cttest`, `ctscript`, `ctdemo`, `ctreload` and `ctsim` as functions, and `ctsim-nogui` and `ctbench` as aliases. On macOS it looks for the application inside the bundle the build produces as well as the one in /Applications. `ctscript --list` names the demonstrations, without their extension, so a line of its output can be pasted straight back to `ctscript`. `ctdemo` runs a script through XWayland on Linux, where Wayland would otherwise ignore the layout a demonstration asks for. The paths are worked out when it runs, so after the first build run `ctreload` to point them at the build directory. `ctbuild` calls `build.sh`, or `build.bat` through cmd when the shell is MSYS2 or Cygwin. `--source-zip` packages the source as a zip and a tarball, for a release page. cpack decides what belongs in one; CPACK_SOURCE_IGNORE_FILES in CMakeLists.txt lists what does not. Asked for on its own it configures but does not compile, since nothing that is built goes into a source package. It takes the tree as it is on disk. When the git repository is reachable again, `git archive` is the better source: it takes exactly what is committed, and cannot pick up a file someone forgot to add. `-j N` sets how many jobs to build with. Both scripts default to every processor the machine has. ## Benchmarking scripts/benchmark.sh --list what there is scripts/benchmark.sh standard -n 5 --threads 1 --csv base.csv before scripts/benchmark.sh standard -n 5 --threads 1 --compare base.csv after Each benchmark is a file in `benchmarks/`: a few lines naming the steps to run and the file each produces. Adding one means writing such a file, not editing the script. name standard describe Rasterize, project and reconstruct at working sizes step rasterize rast.if phm2if rast.if 768 768 --phantom herman --nsample 3 ... quality rec.if rast.if What is reported per step is the tool's own measurement of the calculation -- the clock is around the computation itself, not around reading the options or the input file -- a steady nanosecond clock around the computation, written into the output file's history and read back with `ctsim-nogui ctdump --calc-time` -- which excludes process start-up and file I/O. Wall time is shown too. The `quality` line compares the reconstruction with the phantom, so a change that gains speed by breaking the arithmetic is noticed. Times are printed with enough decimal places for three significant digits at whatever speed the machine runs: a step taking 45 milliseconds shows as 0.0452, not 0.045, so two versions can still be told apart on a quick machine. One decimal count is chosen for the whole table, from its fastest step, so the columns line up. With more than two runs the table also reports the standard deviation of the compute times and the same as a percentage of their mean. That percentage is the number to read before believing a difference: a change smaller than the scatter has not been measured. The `quick` benchmark exists to show what the scatter is when there is almost nothing to compute -- a 1 x 1 rasterization, a few microseconds, which scatters by around 25%, against about 1% for a step of some tens of milliseconds. Use several runs and one thread when comparing two versions of the code. The measurement itself is exact: it is a steady nanosecond clock around the calculation inside the tool, with no shell or process start-up in it. What varies is the calculation, and it varies more when it is short -- the cache starts cold, and a single scheduling interruption is a larger share of a short run. Measured here on an idle machine, twelve runs of the same work: a 1.4 millisecond step spread 11% between its fastest and slowest, a 160 millisecond step 6%. Several runs and the median is the answer to that, and a step of a second or more needs fewer of them. The heading names the commit the tree is at, from `git describe --tags` (`--tags` so that a lightly-made tag counts; without it git reports the nearest *annotated* tag and a count of commits since, which reads as the wrong version), with `-dirty` when there are uncommitted changes -- which is exactly when a result should not be filed away as belonging to a commit. Recorded results carry the same string. ## Demonstrations `demos/intro.ctsim` is the script Help - Run Demonstration runs: a phantom, rasterized, scanned and reconstructed, with captions and pauses. Run it by hand with `ctsim --script scripts/demos/intro.ctsim`. The manual's Scripts section describes the language. ctsim-7.0.0/scripts/benchmark.sh0000755000175000017500000003271015253543770015645 0ustar kevinkevin#!/usr/bin/env bash # # Benchmark CTSim. # # scripts/benchmark.sh the standard benchmark, once # scripts/benchmark.sh --list the benchmarks available # scripts/benchmark.sh filters a named one # scripts/benchmark.sh -n 5 --threads 1 five runs, single-threaded # scripts/benchmark.sh --csv base.csv record the result # scripts/benchmark.sh --compare base.csv show the change against it # scripts/benchmark.sh --help every option # # A benchmark is a file in scripts/benchmarks/*.bench listing the steps to # run. Adding one means writing such a file; this script needs no change. # The format is five line types: # # name what --list and the CSV call it # describe one line, for --list # setup run but not timed, to prepare inputs # step