diff --git a/Modules/IO/IOScanco/CMakeLists.txt b/Modules/IO/IOScanco/CMakeLists.txt new file mode 100644 index 00000000000..225b0bd70d3 --- /dev/null +++ b/Modules/IO/IOScanco/CMakeLists.txt @@ -0,0 +1,4 @@ +project(IOScanco) +set(IOScanco_LIBRARIES IOScanco) + +itk_module_impl() diff --git a/Modules/IO/IOScanco/include/itkAIMHeaderIO.h b/Modules/IO/IOScanco/include/itkAIMHeaderIO.h new file mode 100644 index 00000000000..bf4412144ac --- /dev/null +++ b/Modules/IO/IOScanco/include/itkAIMHeaderIO.h @@ -0,0 +1,127 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ + +#ifndef itkAIMHeaderIO_h +#define itkAIMHeaderIO_h +#include "itkScancoHeaderIO.h" +#include + +struct AIMV020StructHeader; +struct AIMV030StructHeader; + +namespace itk +{ +class AIMHeaderIO : public ScancoHeaderIO +{ +public: + using ScancoHeaderIO::ScancoHeaderIO; + + ~AIMHeaderIO(); + + /** Read the header from an infile. + * Fills in the m_HeaderData structure with the header information. + * \param infile The input file stream to read the header from. + * \throws std::runtime_error if the file cannot be opened or read. + * \note Overrides base class virtual method + */ + unsigned long + ReadHeader(std::ifstream & infile) override; + + /** Write the header to an open file stream. + * \param outfile Pointer to an open std::ofstream where the header will be written. + * \param imageSize size of the image that will be written after the header, in bytes + * \throws std::runtime_error if headerData is null. + * \note overrides base class virtual method + * \returns number of bytes written to the file. + */ + unsigned long + WriteHeader(std::ofstream & outfile, unsigned long imageSize) override; + +protected: + /** Read the AIM pre-header from the file stream. + * \param file The input file stream to read the header from. + * \param offset The offset in the file to start reading from. + * \note m_IntSize is used to determine the size of integers in the pre-header, + * and should be set appropriately before calling + * \returns -1 on failure, 0 on success. + */ + int + ReadPreHeader(std::ifstream & file, size_t offset = 0); + + /** Read the AIM v020 image structure header from a data structure. + * \param headerData pointer to structure containing image structure header + */ + void + ReadImgStructHeader(AIMV020StructHeader * headerData); + + /** Read the AIM v030 image structure header from a data structure. + * \param headerData pointer to structure containing image structure header + * \overload ReadImgStructHeader for AIM v020 + */ + void + ReadImgStructHeader(AIMV030StructHeader * headerData); + + /** Read the AIM processing log from the file stream. + * \param file The input file stream to read the header from. + * \param length The length of the processing log to read. + * \param offset The offset in the file to start reading from. + * \returns size of the processing log read + */ + size_t + ReadProcessingLog(std::ifstream & infile, size_t offset, size_t length); + + /** Write the image structure header to an AIM v020 data structure + * \returns AIMV020StructHeader structure filled with encoded header data + */ + AIMV020StructHeader + WriteStructHeaderV020(); + + /** Write the image structure header to an AIM v030 data structure + * \returns AIMV030StructHeader structure filled with encoded header data + */ + AIMV030StructHeader + WriteStructHeaderV030(); + + /** Write the processing log to a string. + * \returns std::string containing the processing log populated with the header data + */ + std::string + WriteProcessingLog(); + + /** Write the image pre-header to a file + * \param outfile The output file stream to write the pre-header to + * \param imageSize size of the image that will be written after the header, in bytes + * \param version The AIM file version to write (default is AIM_020) + * \returns number of bytes written to the file + * \note pre-header is written into the current file pointer of the outfile stream + * \note pre-header blocks are written based on imageSize, m_ImgStructSize, and m_ProcessingLogSize, + * which should be set accordingly + */ + size_t + WritePreHeader(std::ofstream & outfile, size_t imageSize, ScancoFileVersions version = ScancoFileVersions::AIM_020); + +private: + unsigned int m_IntSize{ 4 }; // Size of integers in the header (4 for AIM v020, 8 for AIM v030) + + /** Header Size = m_PreHeaderSize + m_ImgStructSize + m_ProcessingLogSize */ + size_t m_PreHeaderSize{ 0 }; + size_t m_ImgStructSize{ 0 }; + size_t m_ProcessingLogSize{ 0 }; +}; +} // namespace itk +#endif // itkAIMHeaderIO_h diff --git a/Modules/IO/IOScanco/include/itkISQHeaderIO.h b/Modules/IO/IOScanco/include/itkISQHeaderIO.h new file mode 100644 index 00000000000..c1089e6987f --- /dev/null +++ b/Modules/IO/IOScanco/include/itkISQHeaderIO.h @@ -0,0 +1,118 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ + +#ifndef itkISQHeaderIO_h +#define itkISQHeaderIO_h +#include "itkScancoHeaderIO.h" + +struct ISQEncodedPreHeader; +struct ISQEncodedHeaderBlock; +struct RADEncodedHeaderBlock; + +namespace itk +{ +class ISQHeaderIO : public ScancoHeaderIO +{ +public: + using ScancoHeaderIO::ScancoHeaderIO; + + /** Read the header from an infile. + * Fills in the m_HeaderData structure with the header information. + * \param infile The input file stream to read the header from. + * \throws std::runtime_error if the file cannot be opened or read. + * \note Overrides base class virtual method + */ + unsigned long + ReadHeader(std::ifstream & infile) override; + + /** Write the header to an open file stream. + * \param outfile Pointer to an open std::ofstream where the header will be written. + * \throws std::runtime_error if headerData is null. + * \returns number of bytes written to the file. + */ + unsigned long + WriteHeader(std::ofstream & outfile, unsigned long imageSize) override; + +private: + /** Read date into creation and modification date strings + * \param year The Gregorian year. + * \param month The Gregorian month (1-12). + * \param day The Gregorian day (1-31). + * \param hour The hour (0-23). + * \param minute The minute (0-59). + * \param second The second (0-59). + * \param millis The milliseconds (0-999). + */ + void + ReadDateValues(const int year, + const int month, + const int day, + const int hour, + const int minute, + const int second, + const int milli); + + /** Parse and Save pixel and physical dimension values + * Values are converted into appropriate units + * \see unit conversions in itkScancoImageIO.h + * \param imageData encoded data of at least 24 bytes to read from + * \returns true if the file is a RAD file based on the dimension data, + * false otherise + */ + bool + ReadDimensionData(ISQEncodedPreHeader * imageData); + + /** Read RAD header values and convert units appropriately + * \see unit conversions in itkScancoImageIO.h + * \param headerData struct holding encoded RAD header data + */ + void + ReadRADHeader(RADEncodedHeaderBlock * headerData); + + /** Read ISQ header values and convert units appropriately + * \see unit conversions in itkScancoImageIO.h + * \param headerData struct holding encoded ISQ header data + */ + void + ReadISQHeader(ISQEncodedHeaderBlock * headerData); + + /** Read in extended header data + * This may include a multi-header section with calibration data + * \param buffer pointer to start of extended header data + * \param length length of extended header, + * this can be calculated from the data offset found in the last 4 bytes of the main header block + * \note length must be at least 3 blocks + * \note buffer must be at least length bytes + */ + void + ReadExtendedHeader(const char * buffer, unsigned long length); + + /** Write Calibration data to extended header blocks + * \param outfile file to write data to + * \note requires first block (512 bytes) to be written with header data + * \returns number of bytes written to the file + */ + unsigned long + WriteExtendedHeader(std::ofstream & outfile); + + unsigned long m_HeaderSize; +}; + +} // namespace itk + +#endif // itkISQHeaderIO_h diff --git a/Modules/IO/IOScanco/include/itkScancoDataManipulation.h b/Modules/IO/IOScanco/include/itkScancoDataManipulation.h new file mode 100644 index 00000000000..39d1d7a17be --- /dev/null +++ b/Modules/IO/IOScanco/include/itkScancoDataManipulation.h @@ -0,0 +1,251 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ +#ifndef itkScancoDataManipulation_h +#define itkScancoDataManipulation_h +#include "itkMacro.h" +#include +#include + +struct itkScancoPixelData +{ + int m_Dimensions[3]; // Dimensions of the pixel data + float m_Origin[3]; // Origin of the pixel data in physical space + double m_Spacing[3]; // Spacing between pixels in physical space + int m_ComponentType; // Data Type (e.g., unsigned char, short, float) + int m_PixelType; +}; + +// Scanco header fixed-width fields. The on-disk format stores each text field +// at a fixed binary width (no NUL terminator); the in-memory buffer is two +// bytes wider so that decoded strings are always NUL-terminated. +namespace ScancoHeaderField +{ +inline constexpr std::size_t VersionDiskWidth = 16; +inline constexpr std::size_t VersionBufferSize = VersionDiskWidth + 2; + +inline constexpr std::size_t PatientNameDiskWidth = 40; +inline constexpr std::size_t PatientNameBufferSize = PatientNameDiskWidth + 2; + +inline constexpr std::size_t RescaleUnitsDiskWidth = 16; +inline constexpr std::size_t RescaleUnitsBufferSize = RescaleUnitsDiskWidth + 2; + +inline constexpr std::size_t CalibrationDataDiskWidth = 64; +inline constexpr std::size_t CalibrationDataBufferSize = CalibrationDataDiskWidth + 2; + +// ISQ stores creation/modification timestamps as an 8-byte binary date; in +// memory both ends decode to a 32-byte human-readable date string. +inline constexpr std::size_t EncodedDateDiskWidth = 8; +inline constexpr std::size_t DateStringBufferSize = 32; +} // namespace ScancoHeaderField + +struct itkScancoHeaderData +{ + char m_Version[ScancoHeaderField::VersionBufferSize]; // e.g., "AIMDATA_V020 " + char m_PatientName[ScancoHeaderField::PatientNameBufferSize]; + int m_PatientIndex; + int m_ScannerID; + char m_CreationDate[ScancoHeaderField::DateStringBufferSize]; + char m_ModificationDate[ScancoHeaderField::DateStringBufferSize]; + int m_ScanDimensionsPixels[3]; + double m_ScanDimensionsPhysical[3]; + double m_SliceThickness; // Slice thickness in mm + double m_SliceIncrement; // Slice increment in mm + double m_StartPosition; + double m_EndPosition; + double m_ZPosition; + std::array m_DataRange; + double m_MuScaling; + int m_NumberOfSamples; + int m_NumberOfProjections; + double m_ScanDistance; + double m_SampleTime; + int m_ScannerType; + int m_MeasurementIndex; + int m_Site; + int m_ReconstructionAlg; + double m_ReferenceLine; + double m_Energy; + double m_Intensity; + int m_RescaleType; + char m_RescaleUnits[ScancoHeaderField::RescaleUnitsBufferSize]; + char m_CalibrationData[ScancoHeaderField::CalibrationDataBufferSize]; + double m_RescaleSlope; + double m_RescaleIntercept; + double m_MuWater; + char * m_RawHeader; + itkScancoPixelData m_PixelData; // Pixel data information +}; + +#define ScancoGetConstMacro(name, type) \ + virtual type Get##name() const { return this->m_HeaderData.m_##name; } \ + ITK_MACROEND_NOOP_STATEMENT + +#define ScancoSetMacro(name, type) \ + virtual void Set##name(type _arg) \ + { \ + itkDebugMacro("setting " #name " to " << _arg); \ + ITK_GCC_PRAGMA_PUSH \ + ITK_GCC_SUPPRESS_Wfloat_equal \ + if (this->m_HeaderData.m_##name != _arg) \ + { \ + this->m_HeaderData.m_##name = std::move(_arg); \ + this->Modified(); \ + } \ + ITK_GCC_PRAGMA_POP \ + } \ + ITK_MACROEND_NOOP_STATEMENT + +constexpr int ScancoHeaderBlockSize = 512; + +/** Check the file header to see what type of file it is. + * + * \param header A pointer to the first 16 bytes of the file header. + * \return 0 if unrecognized, 1 if ISQ/RAD, + * 2 if AIM 020, 3 if AIM 030. + */ +int +CheckVersion(const char header[ScancoHeaderField::VersionDiskWidth]); + +/** Convert char data to 32-bit int (little-endian). + * + * \param data Pointer to a buffer of at least 4 bytes. + * \return The decoded integer value. + */ +int +DecodeInt(const void * data); + +/** Convert char data to 64-bit int (little-endian) + * + * \param data Pointer to a buffer of at least 4 bytes. + * \return The decoded integer value. + */ +int64_t +DecodeInt64(const void * data); + +/** Convert 32-bit int (little-endian) to char data. + * + * \param data The integer to convert. + * \param target Pointer to a buffer of at least 4 bytes to store the result. + */ +void +EncodeInt(int data, void * target); + +/** Convert 64-bit int (little-endian) to char data. + * + * \param data The integer to convert. + * \param target Pointer to a buffer of at least 8 bytes to store the result. + */ +void +EncodeInt64(int64_t data, void * target); + +/** Convert char data to float (single precision). + * + * \param data Pointer to a buffer of at least 4 bytes. + * \return The decoded float value. + */ +float +DecodeFloat(const void * data); + +/** Convert float data to char data. + * + * \param data The float to convert. + * \param target Pointer to a buffer of at least 4 bytes to store the result. + */ +void +EncodeFloat(float data, void * target); + +/** Convert char data to float (double precision). + * + * \param data Pointer to a buffer of at least 8 bytes. + * \return The decoded double value. + */ +double +DecodeDouble(const void * data); + +void +EncodeDouble(double data, void * target); + +/** Convert a VMS timestamp to a calendar date. + * + * \param data Pointer to a buffer of at least 8 bytes containing the VMS timestamp. + * \param year The extracted Gregorian year. + * \param month The extracted Gregorian month (1-12). + * \param day The extracted Gregorian day (1-31). + * \param hour The extracted hour (0-23). + * \param minute The extracted minute (0-59). + * \param second The extracted second (0-59). + * \param millis The extracted milliseconds (0-999). + */ +void +DecodeDate(const void * data, int & year, int & month, int & day, int & hour, int & minute, int & second, int & millis); + +/** Formats a date into a string of the form DD-MMM-YYYY HH:MM:SS:mmm + * \param target Pointer to a buffer of at least 32 bytes to store the formatted date string. + * \param year The year. + * \param month The month (1-12). + * \param day The day (1-31). + * \param hour The hour (0-23). + * \param minute The minute (0-59). + * \param second The second (0-59). + * \param millis The milliseconds (0-999). + */ +void +DateToString(const void * target, int year, int month, int day, int hour, int minute, int second, int millis); + +/** Get the current date and time as a string in the format "YYYY-MM-DD HH:MM:SS.mmm". + * \param target Pointer to a buffer of at least 32 bytes to store the current date string. + */ +void +GetCurrentDateString(void * target); + +/** Convert the current calendar date to a VMS timestamp and store in target + * + * \param target Pointer to a buffer of at least 8 bytes to store the VMS timestamp. + */ +void +EncodeCurrentDate(void * target); + +/** Convert a calendar date to VMS timestamp + * + * \param target Pointer to a buffer of at least 8 bytes to store the timestamp. + * \param dateString A string in the format "YYYY-MM-DD HH:MM:SS.mmm" + */ +void +EncodeDateFromString(void * target, const char dateString[ScancoHeaderField::DateStringBufferSize]); + +/** Strip a string by removing trailing whitespace. + * + * \attention dest must have a size of at least l+1. + * \param dest The destination string to be stripped. + * \param source The source string to copy from. + * \param length The total length of the destination string. + */ +void +StripString(char * dest, const char * source, size_t length); + +/** Pad a string with spaces to a specified length. + * + * \attention dest must have a size of at least l+1. + * \param dest The destination string to be padded. + * \param source The source string to copy from. + * \param length The total length of the destination string after padding. + */ +void +PadString(char * dest, const char * source, size_t length); + +#endif // itkScancoDataManipulation_h diff --git a/Modules/IO/IOScanco/include/itkScancoHeaderIO.h b/Modules/IO/IOScanco/include/itkScancoHeaderIO.h new file mode 100644 index 00000000000..7fbbb7a9819 --- /dev/null +++ b/Modules/IO/IOScanco/include/itkScancoHeaderIO.h @@ -0,0 +1,118 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ + +#ifndef itkScancoHeaderIO_h +#define itkScancoHeaderIO_h +#include "itkScancoDataManipulation.h" +#include +#include +#include + +namespace itk +{ +/** \class ScancoHeaderIO + * + * \brief Read Scanco image headers. + * + * \ingroup IOScanco + */ +class ScancoHeaderIO +{ +public: + enum class ScancoFileVersions + { + UNRECOGNIZED = 0, + CTHEADER = 1, + AIM_020 = 2, + AIM_030 = 3 + }; + + /** Constructor that initializes the header data pointer. + * If the pointer is null, the object will fail to be created + * and will throw an exception. + * + * \param headerData Pointer to the location to store the header data + * \note m_RawHeader will be overwritten on read and/or write + * \param filename optional filename to read the header from or write to. + * \ifnot filename is provided, it can be specified later using ReadHeader or WriteHeader + * \see ReadHeader(std::string filename) and WriteHeader(std::string filename) + * \throws std::runtime_error if headerData is null. + */ + ScancoHeaderIO(itkScancoHeaderData * headerData, std::string filename = ""); + + /** Destructor */ + ~ScancoHeaderIO() = default; + + void + SetFilename(const std::string filename); + + void + SetHeaderData(itkScancoHeaderData * headerData); + + /** Read the header from a file. + * If the filename is empty, it will use the m_FileName member variable. + * If m_FileName is also empty, it will throw an exception. + * \param filename The name of the file to read the header from. [optional] + * \note If the filename is provided, it will overwrite the m_FileName member variable. + * \throws std::runtime_error if no filename is provided and m_FileName is empty. + * \returns number of bytes read from the file. + */ + unsigned long + ReadHeader(const std::string filename = ""); + + /** Read the header from an infile. + * \param infile The input file stream to read the header from. + * \throws std::runtime_error if the file cannot be opened or read. + * \note Subclasses should implement this method to read the header + * in the specific format they support. + * \returns number of bytes read from the file. + */ + virtual unsigned long + ReadHeader(std::ifstream & infile); + + + /** Write the header to a file. + * If the filename is empty, it will use the m_FileName member variable. + * If m_FileName is also empty, it will throw an exception. + * \param filename The name of the file to write the header to [optional] + * \param imageSize size of the image that will be written after the header, in bytes + * \note If the filename is provided, it will overwrite the m_FileName member variable. + * \throws std::runtime_error if no filename is provided and m_FileName is empty. + * \returns number of bytes written to the file. + */ + unsigned long + WriteHeader(unsigned long imageSize, std::string filename = ""); + + /** Write the header to an open file stream. + * \param outfile Pointer to an open std::ofstream where the header will be written. + * \param imageSize size of the image that will be written after the header, in bytes + * \throws std::runtime_error if headerData is null. + * \note sublcasses should implement this method to write the header + * in the specific format they support. + * \returns number of bytes written to the file. + */ + virtual unsigned long + WriteHeader(std::ofstream & outfile, unsigned long imageSize); + +protected: + itkScancoHeaderData * m_HeaderData{ nullptr }; // Pointer data structure to be filled with header information + std::string m_FileName = ""; +}; +} // end namespace itk + +#endif // itkScancoHeaderIO_h diff --git a/Modules/IO/IOScanco/include/itkScancoImageIO.h b/Modules/IO/IOScanco/include/itkScancoImageIO.h new file mode 100644 index 00000000000..ef99b6e99cb --- /dev/null +++ b/Modules/IO/IOScanco/include/itkScancoImageIO.h @@ -0,0 +1,384 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ +/*========================================================================= + + Program: DICOM for VTK + + Copyright (c) 2015 David Gobbi + All rights reserved. + See Copyright.txt or http://dgobbi.github.io/bsd3.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + PURPOSE. See the above copyright notice for more information. + +=========================================================================*/ +/** + * David's notes from vtkScancoCTReader + * Read SCANCO ISQ and AIM medical image files + * + * This class reads ISQ and AIM files, which are used for high-resolution + * computed tomography. The information that it provides uses different + * units as compared to the original files: + * + * distances are given in millimeters (instead of micrometers) + * times are given in milliseconds (instead of microseconds) + * voltage and current given in kV and mA (instead of volts and microamps). + * + * If the scanner was calibrated, then + * the data values can be converted to calibrated units. To convert + * to linear attenuation coefficients [cm^-1], simply divide the data + * values by the MuScaling. To convert to density values, multiply + * the data values by the m_RescaleSlope and add the m_RescaleIntercept. + * To convert to Hounsfield units, multiply by 1000/(MuScaling*m_MuWater) + * and subtract 1000. + * + * Created at the Calgary Image Processing and Analysis Centre (CIPAC). + */ +#ifndef itkScancoImageIO_h +#define itkScancoImageIO_h +#include "IOScancoExport.h" + + +#include +#include +#include "itkImageIOBase.h" +#include "itkScancoDataManipulation.h" +#include "itkScancoHeaderIO.h" + +namespace itk +{ +/** \class ScancoImageIO + * + * \brief Read Scanco image file formats. + * + * Many methods are based off vtkScancoCTReader in vtk-dicom by David Gobbi + * + * \ingroup IOFilters + * \ingroup IOScanco + */ +class IOScanco_EXPORT ScancoImageIO : public ImageIOBase +{ +public: + enum ScancoFileExtensions + { + UNRECOGNIZED = -1, + AIM, + ISQ, + RAD, + RSQ + }; + + ITK_DISALLOW_COPY_AND_MOVE(ScancoImageIO); + + /** Standard class typedefs. */ + using Self = ScancoImageIO; + using Superclass = ImageIOBase; + using Pointer = SmartPointer; + using ConstPointer = SmartPointer; + + /** Method for creation through the object factory. */ + itkNewMacro(Self); + + /** Run-time type information (and related methods). */ + itkOverrideGetNameOfClassMacro(ScancoImageIO); + + /** The different types of ImageIO's can support data of varying + * dimensionality. For example, some file formats are strictly 2D + * while others can support 2D, 3D, or even n-D. + * \param dimension The dimension to check for support. + * \return True if the dimension is supported by ScancoIO, false otherwise. + */ + bool + SupportsDimension(unsigned long dimension) override + { + if (dimension == 3) + { + return true; + } + return false; + } + + /*-------- This part of the interfaces deals with reading data. ----- */ + + /** Determine the file type. Returns true if this ImageIO can read the + * file specified. */ + bool + CanReadFile(const char *) override; + + /** Set the spacing and dimension information for the set filename. */ + void + ReadImageInformation() override; + + /** Reads the data from disk into the memory buffer provided. */ + void + Read(void * buffer) override; + + /*-------- This part of the interfaces deals with writing data. ----- */ + + /** Determine the file type. Returns true if this ImageIO can write the + * file specified. */ + bool + CanWriteFile(const char *) override; + + /** Set the spacing and dimension information for the set filename. */ + void + WriteImageInformation() override; + + /** Writes the data to disk from the memory buffer provided. Make sure + * that the IORegions has been set properly. */ + void + Write(const void * buffer) override; + + + bool + CanStreamRead() override + { + return false; + } + + bool + CanStreamWrite() override + { + return false; + } + + /** Get a string that states the version of the file header. + * Max size: 16 characters. */ + const char * + GetVersion() const + { + return this->m_HeaderData.m_Version; + } + void + SetVersion(const char * version) + { + std::snprintf(this->m_HeaderData.m_Version, sizeof(this->m_HeaderData.m_Version), "%s", version); + this->Modified(); + } + + const char * + GetCalibrationData() const + { + return this->m_HeaderData.m_CalibrationData; + } + void + SetCalibrationData(const char * calibrationData) + { + std::snprintf( + this->m_HeaderData.m_CalibrationData, sizeof(this->m_HeaderData.m_CalibrationData), "%s", calibrationData); + this->Modified(); + } + + const char * + GetRescaleUnits() const + { + return this->m_HeaderData.m_RescaleUnits; + } + void + SetRescaleUnits(const char * rescaleUnits) + { + std::snprintf(this->m_HeaderData.m_RescaleUnits, sizeof(this->m_HeaderData.m_RescaleUnits), "%s", rescaleUnits); + this->Modified(); + } + + ScancoGetConstMacro(PatientIndex, int); + ScancoSetMacro(PatientIndex, int); + + ScancoGetConstMacro(ScannerID, int); + ScancoSetMacro(ScannerID, int); + + ScancoGetConstMacro(SliceThickness, double); + ScancoSetMacro(SliceThickness, double); + + ScancoGetConstMacro(SliceIncrement, double); + ScancoSetMacro(SliceIncrement, double); + + ScancoGetConstMacro(StartPosition, double); + ScancoSetMacro(StartPosition, double); + + /** Set / Get the minimum and maximum values */ + const double * + GetDataRange() const + { + return this->m_HeaderData.m_DataRange.data(); + } + void + SetDataRange(const double * dataRange) + { + this->m_HeaderData.m_DataRange[0] = dataRange[0]; + this->m_HeaderData.m_DataRange[1] = dataRange[1]; + } + void + SetDataRange(const std::vector & dataRange) + { + if (dataRange.size() >= 2) + { + this->m_HeaderData.m_DataRange[0] = dataRange[0]; + this->m_HeaderData.m_DataRange[1] = dataRange[1]; + } + } + + ScancoGetConstMacro(MuScaling, double); + ScancoSetMacro(MuScaling, double); + + ScancoGetConstMacro(MuWater, double); + ScancoSetMacro(MuWater, double); + + ScancoGetConstMacro(RescaleType, int); + ScancoSetMacro(RescaleType, int); + + ScancoGetConstMacro(RescaleSlope, double); + ScancoSetMacro(RescaleSlope, double); + + ScancoGetConstMacro(RescaleIntercept, double); + ScancoSetMacro(RescaleIntercept, double); + + ScancoGetConstMacro(NumberOfSamples, int); + ScancoSetMacro(NumberOfSamples, int); + + ScancoGetConstMacro(NumberOfProjections, int); + ScancoSetMacro(NumberOfProjections, int); + + ScancoGetConstMacro(ScanDistance, double); + ScancoSetMacro(ScanDistance, double); + + ScancoGetConstMacro(ScannerType, int); + ScancoSetMacro(ScannerType, int); + + ScancoGetConstMacro(SampleTime, double); + ScancoSetMacro(SampleTime, double); + + ScancoGetConstMacro(MeasurementIndex, int); + ScancoSetMacro(MeasurementIndex, int); + + ScancoGetConstMacro(Site, int); + ScancoSetMacro(Site, int); + + ScancoGetConstMacro(ReferenceLine, int); + ScancoSetMacro(ReferenceLine, int); + + ScancoGetConstMacro(ReconstructionAlg, int); + ScancoSetMacro(ReconstructionAlg, int); + + /** Get a string that states patient name. + * Max size: 40 characters. */ + const char * + GetPatientName() const + { + return this->m_HeaderData.m_PatientName; + } + void + SetPatientName(const char * patientName) + { + std::snprintf(this->m_HeaderData.m_PatientName, sizeof(this->m_HeaderData.m_PatientName), "%s", patientName); + this->Modified(); + } + + const char * + GetCreationDate() const + { + return this->m_HeaderData.m_CreationDate; + } + void + SetCreationDate(const char * creationDate) + { + std::snprintf(this->m_HeaderData.m_CreationDate, sizeof(this->m_HeaderData.m_CreationDate), "%s", creationDate); + this->Modified(); + } + + const char * + GetModificationDate() const + { + return this->m_HeaderData.m_ModificationDate; + } + void + SetModificationDate(const char * modificationDate) + { + std::snprintf( + this->m_HeaderData.m_ModificationDate, sizeof(this->m_HeaderData.m_ModificationDate), "%s", modificationDate); + this->Modified(); + } + + ScancoGetConstMacro(Energy, double); + ScancoSetMacro(Energy, double); + + ScancoGetConstMacro(Intensity, double); + ScancoSetMacro(Intensity, double); + +protected: + ScancoImageIO(); + ~ScancoImageIO() override; + + void + PrintSelf(std::ostream & os, Indent indent) const override; + +private: + /** Rescale the image data to Hounsfield Units */ + template + void + RescaleToHU(TBufferType * buffer, size_t size); + + /** Rescale the image data to Scanco Units + * This is the inverse of RescaleToHU. + * \param buffer Pointer to the buffer containing the image data. + * \param size Size of the buffer in number of elements. + */ + template + void + RescaleToScanco(TBufferType * buffer, size_t size); + + void + InitializeHeader(); + + void + PopulateMetaDataDictionary(); + + void + SetHeaderFromMetaDataDictionary(); + + void + ParseAIMComponentType(int dataType); + + /** Set the IO object based on the image type to read/write + * This method initializes the m_HeaderIO member variable + * This method sets the m_FileExtension member variable + */ + void + SetHeaderIO(); + + /** Set the numeric data type field from the ITK Component Enum */ + void + SetDataTypeFromComponentEnum(); + + itkScancoHeaderData m_HeaderData; + + ScancoHeaderIO * m_HeaderIO{ nullptr }; + + ScancoFileExtensions m_FileExtension = ScancoFileExtensions::UNRECOGNIZED; + + // The compression mode, if any. + int m_Compression; + + bool m_HeaderInitialized = false; + + SizeValueType m_HeaderSize{ 0 }; +}; +} // end namespace itk + +#endif // itkScancoImageIO_h diff --git a/Modules/IO/IOScanco/include/itkScancoImageIOFactory.h b/Modules/IO/IOScanco/include/itkScancoImageIOFactory.h new file mode 100644 index 00000000000..1eb17769574 --- /dev/null +++ b/Modules/IO/IOScanco/include/itkScancoImageIOFactory.h @@ -0,0 +1,70 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ +#ifndef itkScancoImageIOFactory_h +#define itkScancoImageIOFactory_h +#include "IOScancoExport.h" + +#include "itkObjectFactoryBase.h" +#include "itkImageIOBase.h" + +namespace itk +{ +/** \class ScancoImageIOFactory + * \brief Create instances of ScancoImageIO objects using an object factory. + * \ingroup ITKIOScanco + */ +class IOScanco_EXPORT ScancoImageIOFactory : public ObjectFactoryBase +{ +public: + ITK_DISALLOW_COPY_AND_MOVE(ScancoImageIOFactory); + + /** Standard class typedefs. */ + using Self = ScancoImageIOFactory; + using Superclass = ObjectFactoryBase; + using Pointer = SmartPointer; + using ConstPointer = SmartPointer; + + /** Class methods used to interface with the registered factories. */ + const char * + GetITKSourceVersion() const override; + + const char * + GetDescription() const override; + + /** Method for class instantiation. */ + itkFactorylessNewMacro(Self); + + /** Run-time type information (and related methods). */ + itkOverrideGetNameOfClassMacro(ScancoImageIOFactory); + + /** Register one factory of this type */ + static void + RegisterOneFactory() + { + ScancoImageIOFactory::Pointer scancoFactory = ScancoImageIOFactory::New(); + + ObjectFactoryBase::RegisterFactoryInternal(scancoFactory); + } + +protected: + ScancoImageIOFactory(); + ~ScancoImageIOFactory() override = default; +}; +} // end namespace itk + +#endif diff --git a/Modules/IO/IOScanco/itk-module.cmake b/Modules/IO/IOScanco/itk-module.cmake new file mode 100644 index 00000000000..b9dc9b86135 --- /dev/null +++ b/Modules/IO/IOScanco/itk-module.cmake @@ -0,0 +1,21 @@ +# the top-level README is used for describing this module, just +# re-used it for documentation here +# itk_module() defines the module dependencies in IOScanco +# The testing module in IOScanco depends on ITKTestKernel +# By convention those modules outside of ITK are not prefixed with +# ITK. + +# define the dependencies of the include module and the tests +itk_module( + IOScanco + DEPENDS + ITKIOImageBase + TEST_DEPENDS + ITKTestKernel + ITKIOMeta + FACTORY_NAMES + ImageIO::Scanco + DESCRIPTION "Module ingested from upstream." + EXCLUDE_FROM_DEFAULT + ENABLE_SHARED +) diff --git a/Modules/IO/IOScanco/src/CMakeLists.txt b/Modules/IO/IOScanco/src/CMakeLists.txt new file mode 100644 index 00000000000..deeb7f408db --- /dev/null +++ b/Modules/IO/IOScanco/src/CMakeLists.txt @@ -0,0 +1,13 @@ +set( + IOScanco_SRCS + itkScancoDataManipulation.cxx + itkScancoHeaderIO.cxx + itkAIMHeaderIO.cxx + itkISQHeaderIO.cxx + itkScancoImageIO.cxx + itkScancoImageIOFactory.cxx +) + +itk_module_add_library(IOScanco ${IOScanco_SRCS}) + +target_compile_definitions(IOScanco PRIVATE _CRT_SECURE_NO_WARNINGS) diff --git a/Modules/IO/IOScanco/src/itkAIMHeaderIO.cxx b/Modules/IO/IOScanco/src/itkAIMHeaderIO.cxx new file mode 100644 index 00000000000..4903297abef --- /dev/null +++ b/Modules/IO/IOScanco/src/itkAIMHeaderIO.cxx @@ -0,0 +1,697 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ +#include "itkAIMHeaderIO.h" +#include +#include +#include +#include + +constexpr const char * AIM020String = "AIMDATA_V020 "; +constexpr const char * AIM030String = "AIMDATA_V030 "; +constexpr float AIM020WriteVersion = 1.6; + +typedef char EncodedByte; +typedef char EncodedInt4Byte[4]; +typedef char EncodedInt8Byte[8]; +typedef EncodedInt4Byte EncodedTuple4Byte[3]; +typedef EncodedInt8Byte EncodedTuple8Byte[3]; + +struct AIMV020AssociatedData +{ + EncodedInt4Byte m_ID; + EncodedInt4Byte m_Data; + EncodedInt4Byte m_NR; + EncodedInt4Byte m_Size; + EncodedInt4Byte m_Type; +}; + +struct AIMV030AssociatedData +{ + EncodedInt4Byte m_ID; + EncodedInt4Byte m_NR; + EncodedInt4Byte m_Size; + EncodedInt4Byte m_Type; +}; + +struct AIMV020StructHeader +{ + EncodedInt4Byte m_Version; + EncodedInt4Byte m_ProcLog; + EncodedInt4Byte m_Data; + EncodedInt4Byte m_ID; + EncodedInt4Byte m_Reference; + EncodedInt4Byte m_Type; + EncodedTuple4Byte m_Position; + EncodedTuple4Byte m_Dimension; + EncodedTuple4Byte m_Offset; + EncodedTuple4Byte m_SupDimension; + EncodedTuple4Byte m_SupPosition; + EncodedTuple4Byte m_SubDimension; + EncodedTuple4Byte m_TestOffset; + EncodedTuple4Byte m_ElementSize; + AIMV020AssociatedData m_AssociatedData; +}; + +struct AIMV030StructHeader +{ + EncodedInt4Byte m_Version; + EncodedInt4Byte m_ID; + EncodedInt4Byte m_Reference; + EncodedInt4Byte m_Type; + EncodedTuple8Byte m_Position; + EncodedTuple8Byte m_Dimension; + EncodedTuple8Byte m_Offset; + EncodedTuple8Byte m_SupDimension; + EncodedTuple8Byte m_SupPosition; + EncodedTuple8Byte m_SubDimension; + EncodedTuple8Byte m_TestOffset; + EncodedTuple8Byte m_ElementSize; + AIMV030AssociatedData m_AssociatedData; +}; + +struct AIMPreHeaderV020 +{ + EncodedInt4Byte m_PreHeaderLength; + EncodedInt4Byte m_ImageStructLength; + EncodedInt4Byte m_ProcessingLogLength; + EncodedInt4Byte m_ImageDataLength; + EncodedInt4Byte m_AssociatedDataLength; +}; + +struct AIMPreHeaderV030 +{ + EncodedInt8Byte m_PreHeaderLength; + EncodedInt8Byte m_ImageStructLength; + EncodedInt8Byte m_ProcessingLogLength; + EncodedInt8Byte m_ImageDataLength; + EncodedInt8Byte m_AssociatedDataLength; +}; + +namespace itk +{ +unsigned long +AIMHeaderIO::ReadHeader(std::ifstream & infile) +{ + size_t bytesRead = 0; // Use as offset for reading the header + + if (!infile.is_open()) + { + throw std::runtime_error("AIMHeaderIO: Input file stream is not open."); + } + + // Populate the data structure with the raw header information + // Initially read one block to retrieve the header size and version + char * headerBytes = new char[ScancoHeaderBlockSize]; + infile.read(headerBytes, ScancoHeaderBlockSize); + int versionType = CheckVersion(headerBytes); + + // The size of the data values depends on the file version + switch (versionType) + { + case static_cast(ScancoFileVersions::AIM_020): + this->m_IntSize = 4; // AIM v020 uses 32-bit [4 byte] integers + strcpy(this->m_HeaderData->m_Version, AIM020String); + break; + + case static_cast(ScancoFileVersions::AIM_030): + this->m_IntSize = 8; // AIM v030 uses 64-bit [8 byte] integers + strcpy(this->m_HeaderData->m_Version, AIM030String); + bytesRead += ScancoHeaderField::VersionDiskWidth; // Skip the version string + break; + + default: + throw std::runtime_error("AIMHeaderIO: Unrecognized file version."); + break; + } + + // Read the pre-header to get remaining header and log size + + if (this->ReadPreHeader(infile, bytesRead) != 0) + { + throw std::runtime_error("AIMHeaderIO: Failed to read pre-header."); + } + bytesRead += this->m_PreHeaderSize; + + size_t headerSize = this->m_PreHeaderSize + this->m_ImgStructSize + this->m_ProcessingLogSize; + + if (headerSize > static_cast(ScancoHeaderBlockSize)) + { + // Allocate more space for the header and read the rest into the raw header + delete[] headerBytes; + delete[] this->m_HeaderData->m_RawHeader; + const size_t v030Extra = + (versionType == static_cast(ScancoFileVersions::AIM_030)) ? ScancoHeaderField::VersionDiskWidth : 0; + headerBytes = new char[headerSize + v030Extra]; + // headerSize does not include the version string from v030 + infile.seekg(0, std::ios::beg); + infile.read(headerBytes, headerSize + v030Extra); + } + // we have read the full header, save to our data structure + this->m_HeaderData->m_RawHeader = headerBytes; + + // Read the image structure header (version-dependent) + switch (versionType) + { + case static_cast(ScancoFileVersions::AIM_020): + { + AIMV020StructHeader * structHeader = new AIMV020StructHeader; + memcpy((char *)structHeader, headerBytes + bytesRead, this->m_ImgStructSize); + this->ReadImgStructHeader(structHeader); + delete structHeader; + break; + } + case static_cast(ScancoFileVersions::AIM_030): + { + AIMV030StructHeader * structHeader = new AIMV030StructHeader; + memcpy((char *)structHeader, headerBytes + bytesRead, this->m_ImgStructSize); + this->ReadImgStructHeader(structHeader); + delete structHeader; + break; + } + default: + throw std::runtime_error("Error: invalid AIM type."); + break; + } + bytesRead += this->m_ImgStructSize; + + // Read the processing log + this->ReadProcessingLog(infile, bytesRead, this->m_ProcessingLogSize); + bytesRead += this->m_ProcessingLogSize; + + // these items are not in the processing log + this->m_HeaderData->m_SliceThickness = this->m_HeaderData->m_PixelData.m_Spacing[2]; + this->m_HeaderData->m_SliceIncrement = this->m_HeaderData->m_PixelData.m_Spacing[2]; + + return static_cast( + bytesRead); // Return the size of the header, including the version string if written +} + +unsigned long +AIMHeaderIO::WriteHeader(std::ofstream & outfile, unsigned long imageSize) +{ + int bytesWritten = 0; + if (!outfile.is_open()) + { + throw std::runtime_error("AIMHeaderIO: Output file stream is not open."); + } + + outfile.seekp(0, std::ios::beg); + + std::string processingLog = this->WriteProcessingLog(); + this->m_ProcessingLogSize = processingLog.length(); + + // Write the version string if AIM v030 + if (!strcmp(this->m_HeaderData->m_Version, AIM030String)) + { + outfile.write(AIM030String, 16); + bytesWritten += 16; + AIMV030StructHeader structHeader = this->WriteStructHeaderV030(); + this->m_ImgStructSize = sizeof(structHeader); + this->m_PreHeaderSize = this->WritePreHeader(outfile, imageSize, ScancoFileVersions::AIM_030); + outfile.write((char *)&structHeader, this->m_ImgStructSize); + } + else + { + AIMV020StructHeader structHeader = this->WriteStructHeaderV020(); + this->m_ImgStructSize = sizeof(structHeader); + this->m_PreHeaderSize = this->WritePreHeader(outfile, imageSize, ScancoFileVersions::AIM_020); + outfile.write((char *)&structHeader, this->m_ImgStructSize); + } + + outfile.write(processingLog.c_str(), this->m_ProcessingLogSize); + + if (outfile.tellp() != bytesWritten + this->m_PreHeaderSize + this->m_ImgStructSize + this->m_ProcessingLogSize) + { + throw std::runtime_error("Error: write size mismatch"); + } + + return static_cast(bytesWritten + this->m_PreHeaderSize + this->m_ImgStructSize + + this->m_ProcessingLogSize); +} + +int +AIMHeaderIO::ReadPreHeader(std::ifstream & file, size_t offset) +{ + unsigned long bytesRead = this->m_IntSize; // We assume the pre-header length (int) has already been read + + if (!file.is_open()) + { + return -1; + } + + // Seek to the offset to skip the version string if necessary + file.seekg(offset, std::ios::beg); + + if (m_IntSize == 4) + { + // AIM v020 uses 32-bit integers + AIMPreHeaderV020 * headerData = new AIMPreHeaderV020; + file.read((char *)headerData, sizeof(AIMPreHeaderV020)); + this->m_PreHeaderSize = DecodeInt(headerData->m_PreHeaderLength); + if (this->m_PreHeaderSize != sizeof(AIMPreHeaderV020)) + { + return -1; // Invalid pre-header size + } + + this->m_ImgStructSize = DecodeInt(headerData->m_ImageStructLength); + this->m_ProcessingLogSize = DecodeInt(headerData->m_ProcessingLogLength); + + delete headerData; + } + else if (m_IntSize == 8) + { + // AIM v020 uses 32-bit integers + AIMPreHeaderV030 * headerData = new AIMPreHeaderV030; + file.read((char *)headerData, sizeof(AIMPreHeaderV030)); + this->m_PreHeaderSize = DecodeInt64(headerData->m_PreHeaderLength); + if (this->m_PreHeaderSize != sizeof(AIMPreHeaderV030)) + { + return -1; // Invalid pre-header size + } + this->m_ImgStructSize = DecodeInt64(headerData->m_ImageStructLength); + this->m_ProcessingLogSize = DecodeInt64(headerData->m_ProcessingLogLength); + delete headerData; + } + else + { + return -1; // Unsupported integer size + } + + return 0; // Success +} + +void +AIMHeaderIO::ReadImgStructHeader(AIMV020StructHeader * headerData) +{ + + this->m_HeaderData->m_PixelData.m_ComponentType = DecodeInt(headerData->m_Type); + + int i = 0; + for (int & imageDimension : this->m_HeaderData->m_PixelData.m_Dimensions) + { + imageDimension = DecodeInt(headerData->m_Dimension[i]); + i++; + } + + // Element spacing is stored as a float + i = 0; + for (double & spacing : this->m_HeaderData->m_PixelData.m_Spacing) + { + spacing = DecodeFloat(headerData->m_ElementSize[i]); + if (spacing == 0) + { + spacing = 1.0; + } + i++; + } + + // Set the pixel data origin + for (int i = 0; i < 3; ++i) + { + this->m_HeaderData->m_PixelData.m_Origin[i] = + DecodeInt(headerData->m_Position[i]) * this->m_HeaderData->m_PixelData.m_Spacing[i]; + } +} + +void +AIMHeaderIO::ReadImgStructHeader(AIMV030StructHeader * headerData) +{ + this->m_HeaderData->m_PixelData.m_ComponentType = DecodeInt(headerData->m_Type); + + int i = 0; + for (int & imageDimension : this->m_HeaderData->m_PixelData.m_Dimensions) + { + imageDimension = DecodeInt(headerData->m_Dimension[i]); + i++; + } + + // element spacing is a 64-bit integer + i = 0; + for (double & spacing : this->m_HeaderData->m_PixelData.m_Spacing) + { + spacing = 1e-6 * DecodeInt(headerData->m_ElementSize[i]); + if (spacing == 0) + { + spacing = 1.0; + } + i++; + } + + // Set the pixel data origin + for (int i = 0; i < 3; ++i) + { + this->m_HeaderData->m_PixelData.m_Origin[i] = + DecodeInt(headerData->m_Position[i]) * this->m_HeaderData->m_PixelData.m_Spacing[i]; + } +} + +size_t +AIMHeaderIO::ReadProcessingLog(std::ifstream & infile, size_t offset, size_t length) +{ + size_t bytesRead = 0; + std::string readString = ""; + + if (length == 0) + { + return -1; // No image structure header to read + } + + infile.seekg(offset, std::ios::beg); + + while (bytesRead < length) + { + getline(infile, readString); + + if (infile.eof() || infile.fail() || infile.bad()) + { + return -1; // Error reading the file + } + + bytesRead += readString.length() + 1; // +1 for the newline character + + if (readString[0] == '!') + { + // Skip comment lines + continue; + } + + // Assume keys are separated by (at least) two spaces + std::string key = readString.substr(0, readString.find(" ")); + + std::string value = readString.substr(readString.find(" ") + 2); + + if (value.find_first_not_of(" ") == std::string::npos) + { + continue; + } + + value = value.substr(value.find_first_not_of(" "), + value.find_last_not_of(' ') - value.find_first_not_of(" ") + 1); // Trim trailing spaces + + // check for known keys + if (key == "Time") + { + std::snprintf( + this->m_HeaderData->m_ModificationDate, sizeof(this->m_HeaderData->m_ModificationDate), "%s", value.c_str()); + } + else if (key == "Original Creation-Date") + { + std::snprintf( + this->m_HeaderData->m_CreationDate, sizeof(this->m_HeaderData->m_CreationDate), "%s", value.c_str()); + } + else if (key == "Orig-ISQ-Dim-p") + { + size_t processed = 0; + for (int & ScanDimensionsPixel : this->m_HeaderData->m_ScanDimensionsPixels) + { + ScanDimensionsPixel = std::stol(value, &processed); + value = value.substr(processed); + } + } + else if (key == "Orig-ISQ-Dim-um") + { + size_t processed = 0; + for (double & i : this->m_HeaderData->m_ScanDimensionsPhysical) + { + i = stod(value, &processed) * 1e-3; + value = value.substr(processed); + } + } + else if (key == "Patient Name") + { + std::snprintf(this->m_HeaderData->m_PatientName, sizeof(this->m_HeaderData->m_PatientName), "%s", value.c_str()); + } + else if (key == "Index Patient") + { + this->m_HeaderData->m_PatientIndex = stol(value); + } + else if (key == "Index Measurement") + { + this->m_HeaderData->m_MeasurementIndex = stol(value); + } + else if (key == "Site") + { + this->m_HeaderData->m_Site = stol(value); + } + else if (key == "Scanner ID") + { + this->m_HeaderData->m_ScannerID = stol(value); + } + else if (key == "Scanner type") + { + this->m_HeaderData->m_ScannerType = stol(value); + } + else if (key == "Position Slice 1 [um]") + { + this->m_HeaderData->m_StartPosition = stod(value) * 1e-3; + this->m_HeaderData->m_EndPosition = + this->m_HeaderData->m_StartPosition + + this->m_HeaderData->m_PixelData.m_Spacing[2] * (this->m_HeaderData->m_PixelData.m_Dimensions[2] - 1); + } + else if (key == "No. samples") + { + this->m_HeaderData->m_NumberOfSamples = stol(value); + } + else if (key == "No. projections per 180") + { + this->m_HeaderData->m_NumberOfProjections = stol(value); + } + else if (key == "Scan Distance [um]") + { + this->m_HeaderData->m_ScanDistance = stod(value) * 1e-3; + } + else if (key == "Integration time [us]") + { + this->m_HeaderData->m_SampleTime = stod(value) * 1e-3; + } + else if (key == "Reference line [um]") + { + this->m_HeaderData->m_ReferenceLine = stod(value) * 1e-3; + } + else if (key == "Reconstruction-Alg.") + { + this->m_HeaderData->m_ReconstructionAlg = stol(value); + } + else if (key == "Energy [V]") + { + this->m_HeaderData->m_Energy = stod(value) * 1e-3; + } + else if (key == "Intensity [uA]") + { + this->m_HeaderData->m_Intensity = stod(value) * 1e-3; + } + else if (key == "Mu_Scaling") + { + this->m_HeaderData->m_MuScaling = stol(value); + } + else if (key == "Minimum data value") + { + this->m_HeaderData->m_DataRange[0] = stod(value); + } + else if (key == "Maximum data value") + { + this->m_HeaderData->m_DataRange[1] = stod(value); + } + else if (key == "Calib. default unit type") + { + this->m_HeaderData->m_RescaleType = stol(value); + } + else if (key == "Calibration Data") + { + std::snprintf( + this->m_HeaderData->m_CalibrationData, sizeof(this->m_HeaderData->m_CalibrationData), "%s", value.c_str()); + } + else if (key == "Density: unit") + { + std::snprintf( + this->m_HeaderData->m_RescaleUnits, sizeof(this->m_HeaderData->m_RescaleUnits), "%s", value.c_str()); + } + else if (key == "Density: slope") + { + this->m_HeaderData->m_RescaleSlope = stod(value); + } + else if (key == "Density: intercept") + { + this->m_HeaderData->m_RescaleIntercept = stod(value); + } + else if (key == "HU: mu water") + { + this->m_HeaderData->m_MuWater = stod(value); + } + } + return bytesRead; +} + +AIMV020StructHeader +AIMHeaderIO::WriteStructHeaderV020() +{ + AIMV020StructHeader structHeader{ 0 }; + EncodeInt(this->m_HeaderData->m_PixelData.m_ComponentType, structHeader.m_Type); + + EncodeFloat(AIM020WriteVersion, structHeader.m_Version); + + int i = 0; + for (int & imageDimension : this->m_HeaderData->m_PixelData.m_Dimensions) + { + EncodeInt(imageDimension, structHeader.m_Dimension[i]); + i++; + } + + // Element spacing is stored as a float + i = 0; + for (double & spacing : this->m_HeaderData->m_PixelData.m_Spacing) + { + EncodeFloat(spacing, structHeader.m_ElementSize[i]); + i++; + } + + // pixel data origin + for (int i = 0; i < 3; ++i) + { + EncodeInt(this->m_HeaderData->m_PixelData.m_Origin[i] / (float)this->m_HeaderData->m_PixelData.m_Spacing[i], + structHeader.m_Position[i]); + } + return structHeader; +} + +AIMV030StructHeader +AIMHeaderIO::WriteStructHeaderV030() +{ + AIMV030StructHeader structHeader{ 0 }; + EncodeInt(this->m_HeaderData->m_PixelData.m_ComponentType, structHeader.m_Type); + + int i = 0; + for (int & imageDimension : this->m_HeaderData->m_PixelData.m_Dimensions) + { + EncodeInt64((int64_t)imageDimension, structHeader.m_Dimension[i]); + i++; + } + + // element spacing is a 64-bit integer + i = 0; + for (double & spacing : this->m_HeaderData->m_PixelData.m_Spacing) + { + EncodeInt64(spacing * 1e6, structHeader.m_ElementSize[i]); + i++; + } + + // Set the pixel data origin + for (int i = 0; i < 3; ++i) + { + EncodeInt64(this->m_HeaderData->m_PixelData.m_Origin[i] / (float)this->m_HeaderData->m_PixelData.m_Spacing[i], + structHeader.m_Position[i]); + } + + return structHeader; +} + +std::string +AIMHeaderIO::WriteProcessingLog() +{ + std::ostringstream outLog{ "" }; + GetCurrentDateString(this->m_HeaderData->m_ModificationDate); + outLog << std::setprecision(15); + outLog << "! " << std::endl; + outLog << "! Processing Log " << std::endl; + outLog << "!" << std::endl; + outLog << "!-------------------------------------------------------------------------------" << std::endl; + outLog << "Created by ITKIOScanco" << std::endl; + outLog << "Time " << this->m_HeaderData->m_ModificationDate << std::endl; + outLog << "Original Creation-Date " << this->m_HeaderData->m_CreationDate << std::endl; + outLog << "Orig-ISQ-Dim-p " << this->m_HeaderData->m_ScanDimensionsPixels[0] + << " " << this->m_HeaderData->m_ScanDimensionsPixels[1] << " " + << this->m_HeaderData->m_ScanDimensionsPixels[2] << std::endl; + outLog << "Orig-ISQ-Dim-um " << this->m_HeaderData->m_ScanDimensionsPhysical[0] * 1e3 + << " " << this->m_HeaderData->m_ScanDimensionsPhysical[1] * 1e3 << " " + << this->m_HeaderData->m_ScanDimensionsPhysical[2] * 1e3 << std::endl; + outLog << "!-------------------------------------------------------------------------------" << std::endl; + outLog << "Patient Name " << this->m_HeaderData->m_PatientName << std::endl; + outLog << "Index Patient " << this->m_HeaderData->m_PatientIndex << std::endl; + outLog << "Index Measurement " << this->m_HeaderData->m_MeasurementIndex << std::endl; + outLog << "!-------------------------------------------------------------------------------" << std::endl; + outLog << "Site " << this->m_HeaderData->m_Site << std::endl; + outLog << "Scanner ID " << this->m_HeaderData->m_ScannerID << std::endl; + outLog << "Scanner type " << this->m_HeaderData->m_ScannerType << std::endl; + outLog << "Position Slice 1 [um] " << this->m_HeaderData->m_StartPosition * 1e3 << std::endl; + outLog << "No. samples " << this->m_HeaderData->m_NumberOfSamples << std::endl; + outLog << "No. projections per 180 " << this->m_HeaderData->m_NumberOfProjections + << std::endl; + outLog << "Scan Distance [um] " << this->m_HeaderData->m_ScanDistance * 1e3 << std::endl; + outLog << "Integration time [us] " << this->m_HeaderData->m_SampleTime * 1e3 << std::endl; + outLog << "Reference line [um] " << this->m_HeaderData->m_ReferenceLine * 1e3 + << std::endl; + outLog << "Reconstruction-Alg. " << this->m_HeaderData->m_ReconstructionAlg + << std::endl; + outLog << "Energy [V] " << this->m_HeaderData->m_Energy * 1e3 << std::endl; + outLog << "Intensity [uA] " << this->m_HeaderData->m_Intensity * 1e3 << std::endl; + outLog << "!-------------------------------------------------------------------------------" << std::endl; + outLog << "Mu_Scaling " << this->m_HeaderData->m_MuScaling << std::endl; + outLog << "Calibration Data " << this->m_HeaderData->m_CalibrationData << " " << std::endl; + outLog << "Calib. default unit type " << this->m_HeaderData->m_RescaleType + << " " << std::endl; + outLog << "Density: unit " << this->m_HeaderData->m_RescaleUnits + << " " << std::endl; + outLog << "Density: slope " << this->m_HeaderData->m_RescaleSlope << std::endl; + outLog << "Density: intercept " << this->m_HeaderData->m_RescaleIntercept << std::endl; + outLog << "HU: mu water " << this->m_HeaderData->m_MuWater << std::endl; + outLog << "!-------------------------------------------------------------------------------" << std::endl; + outLog << "Minimum data value " << this->m_HeaderData->m_DataRange[0] << std::endl; + outLog << "Maximum data value " << this->m_HeaderData->m_DataRange[1] << std::endl; + + return outLog.str(); +} + +size_t +AIMHeaderIO::WritePreHeader(std::ofstream & outfile, size_t imageSize, ScancoFileVersions version) +{ + if (!outfile.is_open()) + { + return 0; + } + + if (version == ScancoFileVersions::AIM_020) + { + AIMPreHeaderV020 preHeader{ 0 }; + EncodeInt((int)sizeof(AIMPreHeaderV020), preHeader.m_PreHeaderLength); + EncodeInt((int)this->m_ImgStructSize, preHeader.m_ImageStructLength); + EncodeInt((int)this->m_ProcessingLogSize, preHeader.m_ProcessingLogLength); + EncodeInt((int)imageSize, preHeader.m_ImageDataLength); + EncodeInt(0, preHeader.m_AssociatedDataLength); // No associated data handling + outfile.write((char *)&preHeader, sizeof(preHeader)); + return sizeof(preHeader); + } + else if (version == ScancoFileVersions::AIM_030) + { + AIMPreHeaderV030 preHeader{ 0 }; + EncodeInt64(sizeof(AIMPreHeaderV030), preHeader.m_PreHeaderLength); + EncodeInt64(this->m_ImgStructSize, preHeader.m_ImageStructLength); + EncodeInt64(this->m_ProcessingLogSize, preHeader.m_ProcessingLogLength); + EncodeInt64(imageSize, preHeader.m_ImageDataLength); + EncodeInt64(0, preHeader.m_AssociatedDataLength); // No associated data handling + outfile.write((char *)&preHeader, sizeof(preHeader)); + return sizeof(preHeader); + } + else + { + throw std::runtime_error("AIMHeaderIO::WritePreHeader: Invalid AIM file version to write."); + } +} + +AIMHeaderIO::~AIMHeaderIO() {} + +} // namespace itk diff --git a/Modules/IO/IOScanco/src/itkISQHeaderIO.cxx b/Modules/IO/IOScanco/src/itkISQHeaderIO.cxx new file mode 100644 index 00000000000..9e6a9d31d43 --- /dev/null +++ b/Modules/IO/IOScanco/src/itkISQHeaderIO.cxx @@ -0,0 +1,463 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ + +#include "itkISQHeaderIO.h" +#include "itkMath.h" +#include + +typedef char EncodedByte; +typedef EncodedByte EncodedInt[4]; +typedef EncodedInt EncodedIntTuple[3]; +typedef EncodedByte EncodedDouble[8]; + +// Structures to store encoded data to write to a file +// using little-endian format + +struct ISQEncodedPreHeader +{ + EncodedByte m_Version[ScancoHeaderField::VersionDiskWidth]; + EncodedInt m_DataType; + EncodedInt m_ImageSizeBytes; + EncodedInt m_ImageSizeBlocks; + EncodedInt m_PatientIndex; + EncodedInt m_ScannerID; + EncodedByte m_CreationDate[ScancoHeaderField::EncodedDateDiskWidth]; + EncodedIntTuple m_PixelDimensions; + EncodedIntTuple m_PhysicalDimensions; +}; + +struct ISQEncodedHeaderBlock +{ + ISQEncodedPreHeader m_PreHeader; + EncodedInt m_SliceThickness; + EncodedInt m_SliceIncrement; + EncodedInt m_StartPosition; + EncodedInt m_DataMin; + EncodedInt m_DataMax; + EncodedInt m_MuScaling; + EncodedInt m_NumberOfSamples; + EncodedInt m_NumberOfProjections; + EncodedInt m_ScanDistance; + EncodedInt m_ScannerType; + EncodedInt m_SampleTime; + EncodedInt m_MeasurementIndex; + EncodedInt m_Site; + EncodedInt m_ReferenceLine; + EncodedInt m_ReconstructionAlg; + EncodedByte m_PatientName[ScancoHeaderField::PatientNameDiskWidth] = { 0 }; + EncodedInt m_Energy = { 0 }; /* V */ + EncodedInt m_Intensity = { 0 }; /* uA */ + EncodedByte m_Fill[83 * 4] = { 0 }; + EncodedInt m_DataOffset; /* in 512-byte-blocks */ +}; + +struct RADEncodedHeaderBlock +{ + ISQEncodedPreHeader m_PreHeader; + EncodedInt m_MeasurementIndex; + EncodedInt m_DataMin; + EncodedInt m_DataMax; + EncodedInt m_MuScaling; + EncodedByte m_PatientName[ScancoHeaderField::PatientNameDiskWidth] = { 0 }; + EncodedInt m_ZPosition; + EncodedByte m_UnknownFill[4]; + EncodedInt m_SampleTime; + EncodedInt m_Energy = { 0 }; /* V */ + EncodedInt m_Intensity = { 0 }; /* uA */ + EncodedInt m_ReferenceLine; + EncodedInt m_StartPosition; + EncodedInt m_EndPosition; + EncodedByte m_Fill[88 * 4] = { 0 }; + EncodedInt m_DataOffset; /* in 512-byte-blocks */ +}; + +struct ISQCalibrationHeaderBlock +{ + EncodedByte m_Fill1[136]; + EncodedByte m_Title[ScancoHeaderField::VersionDiskWidth]; + EncodedInt m_CalHeaderSize; // size of calibration header in blocks + EncodedByte m_Fill2[356]; + + EncodedByte m_Fill3[28]; + EncodedByte m_CalibrationData[ScancoHeaderField::CalibrationDataDiskWidth]; + EncodedByte m_Fill4[420]; + + EncodedByte m_Fill5[120]; + EncodedInt m_RescaleType; + EncodedByte m_Fill6[12]; + EncodedByte m_RescaleUnits[ScancoHeaderField::RescaleUnitsDiskWidth]; + EncodedDouble m_RescaleSlope; + EncodedDouble m_RescaleIntercept; + EncodedByte m_Fill7[8]; + EncodedDouble m_MuWater; + EncodedByte m_Fill8[328]; +}; + +constexpr const char * ISQVersionString = "CTDATA-HEADER_V1"; + +namespace itk +{ + +unsigned long +ISQHeaderIO::ReadHeader(std::ifstream & infile) +{ + if (!infile.is_open()) + { + throw std::runtime_error("ISQHeaderIO: Input file stream is not open."); + } + + // Populate the data structure with the raw header information + // Initially read 'pre header' block to retrieve the header size and version + ISQEncodedPreHeader * imageInfo = new ISQEncodedPreHeader; + infile.read((char *)imageInfo, sizeof(ISQEncodedPreHeader)); + + int versionType = CheckVersion(imageInfo->m_Version); + + if (versionType != static_cast(ScancoFileVersions::CTHEADER)) + { + throw std::runtime_error("ISQHeaderIO: cannot read file, is not an ISQ."); + } + + memcpy(this->m_HeaderData->m_Version, imageInfo->m_Version, ScancoHeaderField::VersionDiskWidth); + this->m_HeaderData->m_Version[ScancoHeaderField::VersionDiskWidth] = '\0'; // ensure null-terminated string + this->m_HeaderData->m_PixelData.m_ComponentType = DecodeInt(imageInfo->m_DataType); + // Ignore image blocks and bytes size information + // This will be re-populated on write + this->m_HeaderData->m_PatientIndex = DecodeInt(imageInfo->m_PatientIndex); + this->m_HeaderData->m_ScannerID = DecodeInt(imageInfo->m_ScannerID); + int year, month, day, hour, minute, second, milli; + DecodeDate(imageInfo->m_CreationDate, year, month, day, hour, minute, second, milli); + this->ReadDateValues(year, month, day, hour, minute, second, milli); + + bool isRAD = this->ReadDimensionData(imageInfo); + + delete imageInfo; + if (isRAD) + { + RADEncodedHeaderBlock * headerInfo = new RADEncodedHeaderBlock; + infile.seekg(0, std::ios::beg); + infile.read((char *)headerInfo, sizeof(RADEncodedHeaderBlock)); + this->ReadRADHeader(headerInfo); + delete headerInfo; + } + else + { + ISQEncodedHeaderBlock * headerInfo = new ISQEncodedHeaderBlock; + infile.seekg(0, std::ios::beg); + infile.read((char *)headerInfo, sizeof(ISQEncodedHeaderBlock)); + this->ReadISQHeader(headerInfo); + delete headerInfo; + } + + // Check if there is extended header data + if (this->m_HeaderSize > sizeof(ISQEncodedHeaderBlock)) + { + // clear the buffers in case they were previously full + delete[] this->m_HeaderData->m_RawHeader; + this->m_HeaderData->m_RawHeader = new char[this->m_HeaderSize]; + infile.seekg(0, std::ios::beg); + infile.read(this->m_HeaderData->m_RawHeader, this->m_HeaderSize); + if (static_cast(infile.gcount()) < this->m_HeaderSize) + { + return 0; + } + } + + this->ReadExtendedHeader(this->m_HeaderData->m_RawHeader + sizeof(ISQEncodedHeaderBlock), + this->m_HeaderSize - sizeof(ISQEncodedHeaderBlock)); + + return this->m_HeaderSize; +} + +unsigned long +ISQHeaderIO::WriteHeader(std::ofstream & outfile, unsigned long imageSize) +{ + // Write Normal Header + unsigned long bytesWritten = 0; + outfile.seekp(0, std::ios::beg); + + if (imageSize == 0) + { + throw std::runtime_error("Cannot write to file, no image data length set"); + return 0; + } + + ISQEncodedHeaderBlock header = { 0 }; + + PadString(header.m_PreHeader.m_Version, this->m_HeaderData->m_Version, ScancoHeaderField::VersionDiskWidth); + EncodeInt(3, header.m_PreHeader.m_DataType); + EncodeInt(imageSize, header.m_PreHeader.m_ImageSizeBytes); + EncodeInt(imageSize / ScancoHeaderBlockSize, header.m_PreHeader.m_ImageSizeBlocks); + EncodeInt(this->m_HeaderData->m_PatientIndex, header.m_PreHeader.m_PatientIndex); + EncodeInt(this->m_HeaderData->m_ScannerID, header.m_PreHeader.m_ScannerID); + EncodeDateFromString(header.m_PreHeader.m_CreationDate, this->m_HeaderData->m_CreationDate); + + for (unsigned int dimension = 0; dimension < 3; ++dimension) + { + // pixdim + EncodeInt(this->m_HeaderData->m_PixelData.m_Dimensions[dimension], header.m_PreHeader.m_PixelDimensions[dimension]); + } + for (unsigned int dimension = 0; dimension < 3; ++dimension) + { + // physdim + EncodeInt(this->m_HeaderData->m_PixelData.m_Spacing[dimension] * m_HeaderData->m_PixelData.m_Dimensions[dimension] * + 1e3, + header.m_PreHeader.m_PhysicalDimensions[dimension]); + } + EncodeInt((int)(this->m_HeaderData->m_SliceThickness * 1e3), header.m_SliceThickness); + EncodeInt((int)(this->m_HeaderData->m_SliceIncrement * 1e3), header.m_SliceIncrement); + EncodeInt((int)(this->m_HeaderData->m_StartPosition * 1e3), header.m_StartPosition); + EncodeInt((int)(this->m_HeaderData->m_DataRange[0]), header.m_DataMin); + EncodeInt((int)(this->m_HeaderData->m_DataRange[1]), header.m_DataMax); + EncodeInt((int)this->m_HeaderData->m_MuScaling, header.m_MuScaling); + EncodeInt(this->m_HeaderData->m_NumberOfSamples, header.m_NumberOfSamples); + EncodeInt(this->m_HeaderData->m_NumberOfProjections, header.m_NumberOfProjections); + EncodeInt((int)(this->m_HeaderData->m_ScanDistance * 1e3), header.m_ScanDistance); + EncodeInt((int)(this->m_HeaderData->m_ScannerType), header.m_ScannerType); + EncodeInt((int)(this->m_HeaderData->m_SampleTime * 1e3), header.m_SampleTime); + EncodeInt((int)(this->m_HeaderData->m_MeasurementIndex), header.m_MeasurementIndex); + EncodeInt((int)(this->m_HeaderData->m_Site), header.m_Site); + EncodeInt((int)(this->m_HeaderData->m_ReferenceLine * 1e3), header.m_ReferenceLine); + EncodeInt((int)(this->m_HeaderData->m_ReconstructionAlg), header.m_ReconstructionAlg); + PadString(header.m_PatientName, this->m_HeaderData->m_PatientName, ScancoHeaderField::PatientNameDiskWidth); + EncodeInt((int)(this->m_HeaderData->m_Energy * 1e3), header.m_Energy); + EncodeInt((int)(this->m_HeaderData->m_Intensity * 1e3), header.m_Intensity); + const std::size_t fillSize = 83 * 4; + std::memset(header.m_Fill, 0x00, fillSize); + + EncodeInt(4, header.m_DataOffset); // We will add 4 block offset for the extended header + + outfile.write((char *)&header, ScancoHeaderBlockSize); + // Write Extended Header + unsigned long extendedHeaderLength = this->WriteExtendedHeader(outfile); + bytesWritten += extendedHeaderLength + ScancoHeaderBlockSize; + + return bytesWritten; +} + +void +ISQHeaderIO::ReadDateValues(const int year, + int month, + const int day, + const int hour, + const int minute, + const int second, + const int milli) +{ + // Convert date information into a string + month = ((month > 12 || month < 1) ? 0 : month); + static const char * months[] = { "XXX", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", + "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; + DateToString(this->m_HeaderData->m_CreationDate, year, month, day, hour, minute, second, milli); + DateToString(this->m_HeaderData->m_ModificationDate, year, month, day, hour, minute, second, milli); +} + +bool +ISQHeaderIO::ReadDimensionData(ISQEncodedPreHeader * imageData) +{ + int pixdim[3]; + int physdim[3]; + for (int i = 0; i < 3; i++) + { + pixdim[i] = DecodeInt(imageData->m_PixelDimensions[i]); + } + + for (int i = 0; i < 3; i++) + { + physdim[i] = DecodeInt(imageData->m_PhysicalDimensions[i]); + } + + const bool isRAD = (this->m_HeaderData->m_PixelData.m_ComponentType == 9 || physdim[2] == 0); + + // Perform a sanity check on the dimensions + for (int i = 0; i < 3; ++i) + { + this->m_HeaderData->m_ScanDimensionsPixels[i] = pixdim[i]; + if (pixdim[i] < 1) + { + pixdim[i] = 1; + } + this->m_HeaderData->m_PixelData.m_Dimensions[i] = pixdim[i]; + + this->m_HeaderData->m_ScanDimensionsPhysical[i] = (isRAD ? physdim[i] * 1e-6 : physdim[i] * 1e-3); + if (physdim[i] == 0) + { + physdim[i] = 1.0; + } + + this->m_HeaderData->m_PixelData.m_Spacing[i] = + (isRAD && i == 2) ? 1.0 + : this->m_HeaderData->m_ScanDimensionsPhysical[i] / + this->m_HeaderData->m_ScanDimensionsPixels[i]; // RAD doesn't have a third dimension + + this->m_HeaderData->m_PixelData.m_Origin[i] = 0.0; + } + + return isRAD; +} + +void +ISQHeaderIO::ReadRADHeader(RADEncodedHeaderBlock * headerData) +{ + this->m_HeaderData->m_MeasurementIndex = DecodeInt(headerData->m_MeasurementIndex); + this->m_HeaderData->m_DataRange[0] = DecodeInt(headerData->m_DataMin); + this->m_HeaderData->m_DataRange[1] = DecodeInt(headerData->m_DataMax); + this->m_HeaderData->m_MuScaling = DecodeInt(headerData->m_MuScaling); + StripString(this->m_HeaderData->m_PatientName, headerData->m_PatientName, ScancoHeaderField::PatientNameDiskWidth); + this->m_HeaderData->m_ZPosition = DecodeInt(headerData->m_ZPosition) * 1e-3; + this->m_HeaderData->m_SampleTime = DecodeInt(headerData->m_SampleTime) * 1e-3; + this->m_HeaderData->m_Energy = DecodeInt(headerData->m_Energy) * 1e-3; + this->m_HeaderData->m_Intensity = DecodeInt(headerData->m_Intensity) * 1e-3; + this->m_HeaderData->m_ReferenceLine = DecodeInt(headerData->m_ReferenceLine) * 1e-3; + this->m_HeaderData->m_StartPosition = DecodeInt(headerData->m_StartPosition) * 1e-3; + this->m_HeaderData->m_EndPosition = DecodeInt(headerData->m_EndPosition) * 1e-3; + this->m_HeaderSize = (DecodeInt(headerData->m_DataOffset) + 1) * 512; +} + +void +ISQHeaderIO::ReadISQHeader(ISQEncodedHeaderBlock * headerData) +{ + this->m_HeaderData->m_SliceThickness = DecodeInt(headerData->m_SliceThickness) * 1e-3; + this->m_HeaderData->m_SliceIncrement = DecodeInt(headerData->m_SliceIncrement) * 1e-3; + + this->m_HeaderData->m_StartPosition = DecodeInt(headerData->m_StartPosition) * 1e-3; + + this->m_HeaderData->m_EndPosition = + this->m_HeaderData->m_StartPosition + this->m_HeaderData->m_ScanDimensionsPhysical[2] * + (this->m_HeaderData->m_ScanDimensionsPixels[2] - 1) / + this->m_HeaderData->m_ScanDimensionsPixels[2]; + this->m_HeaderData->m_DataRange[0] = DecodeInt(headerData->m_DataMin); + this->m_HeaderData->m_DataRange[1] = DecodeInt(headerData->m_DataMax); + this->m_HeaderData->m_MuScaling = DecodeInt(headerData->m_MuScaling); + this->m_HeaderData->m_NumberOfSamples = DecodeInt(headerData->m_NumberOfSamples); + this->m_HeaderData->m_NumberOfProjections = DecodeInt(headerData->m_NumberOfProjections); + this->m_HeaderData->m_ScanDistance = DecodeInt(headerData->m_ScanDistance) * 1e-3; + this->m_HeaderData->m_ScannerType = DecodeInt(headerData->m_ScannerType); + this->m_HeaderData->m_SampleTime = DecodeInt(headerData->m_SampleTime) * 1e-3; + this->m_HeaderData->m_MeasurementIndex = DecodeInt(headerData->m_MeasurementIndex); + this->m_HeaderData->m_Site = DecodeInt(headerData->m_Site); + this->m_HeaderData->m_ReferenceLine = DecodeInt(headerData->m_ReferenceLine) * 1e-3; + this->m_HeaderData->m_ReconstructionAlg = DecodeInt(headerData->m_ReconstructionAlg); + StripString(this->m_HeaderData->m_PatientName, headerData->m_PatientName, ScancoHeaderField::PatientNameDiskWidth); + this->m_HeaderData->m_Energy = DecodeInt(headerData->m_Energy) * 1e-3; + this->m_HeaderData->m_Intensity = DecodeInt(headerData->m_Intensity) * 1e-3; + this->m_HeaderSize = (DecodeInt(headerData->m_DataOffset) + 1) * 512; + + // fix m_SliceThickness and m_SliceIncrement if they were truncated + double computedSpacing = + this->m_HeaderData->m_ScanDimensionsPhysical[2] / this->m_HeaderData->m_ScanDimensionsPixels[2]; + if (itk::Math::abs(computedSpacing - this->m_HeaderData->m_SliceThickness) < 1.1e-3) + { + this->m_HeaderData->m_SliceThickness = computedSpacing; + } + if (itk::Math::abs(computedSpacing - this->m_HeaderData->m_SliceIncrement) < 1.1e-3) + { + this->m_HeaderData->m_SliceIncrement = computedSpacing; + } +} + +void +ISQHeaderIO::ReadExtendedHeader(const char * buffer, unsigned long length) +{ + unsigned long bytesRead = 0; + unsigned long hSkip = 0; // track skipped header blocks + unsigned long hBlocks = 0; + ISQCalibrationHeaderBlock * calHeader = nullptr; + int calHeaderSize = 0; + + if (length < ScancoHeaderBlockSize * 3) + { + return; + } + + if (strncmp(buffer + 8, "MultiHeader ", 16) == 0) + { + bytesRead += ScancoHeaderBlockSize; // Skip multiheader if it exists + hSkip += 1; + } + + for (int i = 0; i < 4; ++i) + { + const char * headerName = buffer + bytesRead + (i * 128) + 8; + if (strncmp(headerName, "Calibration ", 16) == 0) + { + hBlocks = DecodeInt(buffer + bytesRead + (i * 128) + 24); + if ((hSkip + hBlocks) * ScancoHeaderBlockSize > length) + { + break; + } + calHeader = new ISQCalibrationHeaderBlock; + memcpy((char *)calHeader, buffer + (hSkip * ScancoHeaderBlockSize), sizeof(ISQCalibrationHeaderBlock)); + calHeaderSize = hBlocks * ScancoHeaderBlockSize; + break; + } + hSkip += hBlocks; + bytesRead += hBlocks; + } + + if (calHeader && calHeaderSize >= 1024) + { + // Read Calibration data from header + StripString( + this->m_HeaderData->m_CalibrationData, calHeader->m_CalibrationData, ScancoHeaderField::CalibrationDataDiskWidth); + // std::string calFile(h + 112, 256); + // std::string s3(h + 376, 256); + this->m_HeaderData->m_RescaleType = DecodeInt(calHeader->m_RescaleType); + StripString( + this->m_HeaderData->m_RescaleUnits, calHeader->m_RescaleUnits, ScancoHeaderField::RescaleUnitsDiskWidth); + // std::string s5(h + 700, 16); + // std::string calFilter(h + 772, 16); + this->m_HeaderData->m_RescaleSlope = DecodeDouble(calHeader->m_RescaleSlope); + this->m_HeaderData->m_RescaleIntercept = DecodeDouble(calHeader->m_RescaleIntercept); + this->m_HeaderData->m_MuWater = DecodeDouble(calHeader->m_MuWater); + } + + delete calHeader; +} + +unsigned long +ISQHeaderIO::WriteExtendedHeader(std::ofstream & outfile) +{ + ISQCalibrationHeaderBlock calHeader = { 0 }; + outfile.seekp(ScancoHeaderBlockSize, std::ios::beg); // seek past first header block + + // First block adds multiheader + char * buffer = new char[ScancoHeaderBlockSize]; + memset(buffer, 0x00, ScancoHeaderBlockSize); + memcpy(buffer + 8, "MultiHeader ", 16); + outfile.write(buffer, ScancoHeaderBlockSize); + delete[] buffer; + + // Next block adds calibration data + memcpy(calHeader.m_Title, "Calibration ", 16); + PadString( + calHeader.m_CalibrationData, this->m_HeaderData->m_CalibrationData, ScancoHeaderField::CalibrationDataDiskWidth); + EncodeInt(2, calHeader.m_CalHeaderSize); + + // Last block adds density scaling data: + EncodeInt((int)this->m_HeaderData->m_RescaleType, calHeader.m_RescaleType); + PadString(calHeader.m_RescaleUnits, this->m_HeaderData->m_RescaleUnits, ScancoHeaderField::RescaleUnitsDiskWidth); + EncodeDouble(this->m_HeaderData->m_RescaleSlope, calHeader.m_RescaleSlope); + EncodeDouble(this->m_HeaderData->m_RescaleIntercept, calHeader.m_RescaleIntercept); + EncodeDouble(this->m_HeaderData->m_MuWater, calHeader.m_MuWater); + outfile.write((char *)&calHeader, sizeof(ISQCalibrationHeaderBlock)); + + return ScancoHeaderBlockSize + sizeof(ISQCalibrationHeaderBlock); +} + +} // namespace itk diff --git a/Modules/IO/IOScanco/src/itkScancoDataManipulation.cxx b/Modules/IO/IOScanco/src/itkScancoDataManipulation.cxx new file mode 100644 index 00000000000..a78c0a9593a --- /dev/null +++ b/Modules/IO/IOScanco/src/itkScancoDataManipulation.cxx @@ -0,0 +1,373 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ +#include "itkScancoDataManipulation.h" +#include +#include +#include +#include +#include // for sscanf +#include // for std::runtime_error +#include // for std::string +#include + +/** VMS time conversion constants */ +/** This is the offset between the astronomical "Julian day", which counts + * days since January 1, 4713BC, and the "VMS epoch", which counts from + * November 17, 1858: + */ +constexpr uint64_t julianOffset = 2400001; +constexpr uint64_t millisPerSecond = 1000; +constexpr uint64_t millisPerMinute = 60 * 1000; +constexpr uint64_t millisPerHour = 3600 * 1000; +constexpr uint64_t millisPerDay = 3600 * 24 * 1000; + +const std::map monthIndex{ { "XXX", 0 }, { "JAN", 1 }, { "FEB", 2 }, { "MAR", 3 }, { "APR", 4 }, + { "MAY", 5 }, { "JUN", 6 }, { "JUL", 7 }, { "AUG", 8 }, { "SEP", 9 }, + { "OCT", 10 }, { "NOV", 11 }, { "DEC", 12 } }; + +static constexpr const char * monthStrings[] = { "XXX", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", + "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; + +int +CheckVersion(const char header[ScancoHeaderField::VersionDiskWidth]) +{ + int fileType = 0; + + if (strncmp(header, "CTDATA-HEADER_V1", ScancoHeaderField::VersionDiskWidth) == 0) + { + fileType = 1; + } + else if (strncmp(header, "AIMDATA_V030 ", ScancoHeaderField::VersionDiskWidth) == 0) + { + fileType = 3; + } + else if (strncmp(header, "AIMDATA_V020 ", ScancoHeaderField::VersionDiskWidth) == 0) + { + fileType = 2; + } + else + { + // Legacy pre-magic AIM v020 files start with the pre-header byte count + // (20 = 0x14) in little-endian uint32. Recognize that structural signature + // instead of self-nominating for arbitrary unrelated content. + const auto * bytes = reinterpret_cast(header); + const uint32_t preHeaderSize = bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24); + if (preHeaderSize == 20) + { + fileType = 2; + } + } + + return fileType; +} + +int +DecodeInt(const void * data) +{ + const auto * cp = static_cast(data); + return (cp[0] | (cp[1] << 8) | (cp[2] << 16) | (cp[3] << 24)); +} + +int64_t +DecodeInt64(const void * data) +{ + const auto * cp = static_cast(data); + return (static_cast(cp[0]) | (static_cast(cp[1]) << 8) | (static_cast(cp[2]) << 16) | + (static_cast(cp[3]) << 24) | (static_cast(cp[4]) << 32) | + (static_cast(cp[5]) << 40) | (static_cast(cp[6]) << 48) | + (static_cast(cp[7]) << 56)); +} + +void +EncodeInt(int data, void * target) +{ + auto * targetAsUnsignedChar = static_cast(target); + targetAsUnsignedChar[0] = (unsigned char)(data); + targetAsUnsignedChar[1] = (unsigned char)(data >> 8); + targetAsUnsignedChar[2] = (unsigned char)(data >> 16); + targetAsUnsignedChar[3] = (unsigned char)(data >> 24); +} + +void +EncodeInt64(int64_t data, void * target) +{ + auto * targetAsUnsignedChar = static_cast(target); + targetAsUnsignedChar[0] = (unsigned char)(data); + targetAsUnsignedChar[1] = (unsigned char)(data >> 8); + targetAsUnsignedChar[2] = (unsigned char)(data >> 16); + targetAsUnsignedChar[3] = (unsigned char)(data >> 24); + targetAsUnsignedChar[4] = (unsigned char)(data >> 32); + targetAsUnsignedChar[5] = (unsigned char)(data >> 40); + targetAsUnsignedChar[6] = (unsigned char)(data >> 48); + targetAsUnsignedChar[7] = (unsigned char)(data >> 56); +} + +float +DecodeFloat(const void * data) +{ + const auto * cp = static_cast(data); + // different ordering and exponent bias than IEEE 754 float + union + { + float f; + unsigned int i; + } v; + v.i = (cp[0] << 16) | (cp[1] << 24) | cp[2] | (cp[3] << 8); + return 0.25 * v.f; +} + +void +EncodeFloat(float data, void * target) +{ + // Reverse the scaling applied in DecodeFloat + union + { + float f; + unsigned int i; + } v; + v.f = data / 0.25; + + auto * targetUnsigned = static_cast(target); + targetUnsigned[0] = (unsigned char)(v.i >> 16); + targetUnsigned[1] = (unsigned char)(v.i >> 24); + targetUnsigned[2] = (unsigned char)(v.i); + targetUnsigned[3] = (unsigned char)(v.i >> 8); +} + + +double +DecodeDouble(const void * data) +{ + // different ordering and exponent bias than IEEE 754 double + const auto * cp = static_cast(data); + union + { + double d; + uint64_t l; + } v; + unsigned int l1, l2; + l1 = (cp[0] << 16) | (cp[1] << 24) | cp[2] | (cp[3] << 8); + l2 = (cp[4] << 16) | (cp[5] << 24) | cp[6] | (cp[7] << 8); + v.l = (static_cast(l1) << 32) | l2; + return v.d * 0.25; +} + +void +EncodeDouble(double data, void * target) +{ + // Reverse the scaling applied in DecodeDouble + union + { + double d; + uint64_t l; + } v; + v.d = data / 0.25; + + unsigned int l1 = static_cast(v.l >> 32); + unsigned int l2 = static_cast(v.l & 0xFFFFFFFF); + + auto * cp = static_cast(target); + cp[0] = (unsigned char)(l1 >> 16); + cp[1] = (unsigned char)(l1 >> 24); + cp[2] = (unsigned char)(l1); + cp[3] = (unsigned char)(l1 >> 8); + + cp[4] = (unsigned char)(l2 >> 16); + cp[5] = (unsigned char)(l2 >> 24); + cp[6] = (unsigned char)(l2); + cp[7] = (unsigned char)(l2 >> 8); +} + +/** This algorithm is from Henry F. Fliegel and Thomas C. Van Flandern + * This uses the Gregorian calendar starting from October 15, 1582 + * \param julianDay The Julian day vaulue to convert + * \param year The extracted Gregorian year + * \param month The extracted Gregorian month + * \param day The extracted Gregorian day + */ +void +GregorianDateFromJulian(int julianDay, int & year, int & month, int & day) +{ + int ell, n, i, j = 0; + ell = julianDay + 68569; + n = (4 * ell) / 146097; + ell = ell - (146097 * n + 3) / 4; + i = (4000 * (ell + 1)) / 1461001; + ell = ell - (1461 * i) / 4 + 31; + j = (80 * ell) / 2447; + day = ell - (2447 * j) / 80; + ell = j / 11; + month = j + 2 - (12 * ell); + year = 100 * (n - 49) + i + ell; +} + +void +DecodeDate(const void * data, int & year, int & month, int & day, int & hour, int & minute, int & second, int & millis) +{ + // Read the date as a long integer with units of 1e-7 seconds + int d1 = DecodeInt(data); + int d2 = DecodeInt(static_cast(data) + 4); + uint64_t tVMS = d1 + (static_cast(d2) << 32); + uint64_t time = tVMS / 10000 + julianOffset * millisPerDay; + + // Extract the number of days since the Julian epoch + int julianDay = static_cast(time / millisPerDay); + time -= millisPerDay * julianDay; + + GregorianDateFromJulian(julianDay, year, month, day); + + // Convert the remaining time into hours, minutes, seconds, and milliseconds + hour = static_cast(time / millisPerHour); + time -= hour * millisPerHour; + minute = static_cast(time / millisPerMinute); + time -= minute * millisPerMinute; + second = static_cast(time / millisPerSecond); + time -= second * millisPerSecond; + millis = static_cast(time); +} + +void +DateToString(const void * target, int year, int month, int day, int hour, int minute, int second, int millis) +{ + snprintf((char *)target, + 32, + "%d-%s-%d %02d:%02d:%02d.%03d", + (day % 100), + monthStrings[month], + (year % 10000), + (hour % 100), + (minute % 100), + (second % 100), + (millis % 1000)); +} + +void +GetCurrentDateString(void * target) +{ + time_t rawtime; + struct tm * timeinfo; + + std::time(&rawtime); + timeinfo = localtime(&rawtime); + // tm_year gives years since 1900 + DateToString(target, + timeinfo->tm_year + 1900, + timeinfo->tm_mon, + timeinfo->tm_mday, + timeinfo->tm_hour, + timeinfo->tm_min, + timeinfo->tm_sec, + 0); +} + +void +EncodeCurrentDate(void * target) +{ + char * dateString = new char[ScancoHeaderField::DateStringBufferSize]; + GetCurrentDateString(dateString); + EncodeDateFromString(target, dateString); + delete[] dateString; +} + +/** This algorithm is from Henry F. Fliegel and Thomas C. Van Flandern + * It converts a Gregorian date to a Julian day number. + * + * \param year The year in the Gregorian calendar + * \param month The month in the Gregorian calendar + * \param day The day in the Gregorian calendar + */ +int +julianDayFromDate(int year, int month, int day) +{ + // If month is January or February, treat them as months 13 and 14 of the previous year + if (month <= 2) + { + year -= 1; + month += 12; + } + int a = year / 100; + int b = 2 - a + (a / 4); + int julianDay = static_cast(365.25 * (year + 4716)) + static_cast(30.6001 * (month + 1)) + day + b - 1524.5; + return julianDay; +} + +void +EncodeDateFromString(void * target, const char dateString[ScancoHeaderField::DateStringBufferSize]) +{ + int year, day, hour, minute, second, millis = 0; + char monthStr[4]; + if (sscanf(dateString, "%d-%3s-%d %d:%d:%d.%d", &day, monthStr, &year, &hour, &minute, &second, &millis) != 7) + { + throw std::runtime_error("Invalid date string format. Expected format: YYYY-MMM-DD HH:MM:SS.mmm"); + } + + int month = 0; + const auto iter = monthIndex.find(monthStr); + + if (iter != monthIndex.cend()) + { + month = iter->second; + } + else + { + month = 0; + } + + // add time values to single time total in ms + uint64_t timestamp = hour * millisPerHour + minute * millisPerMinute + second * millisPerSecond + millis; + + // Calculate the Julian day from the date + int julianDay = julianDayFromDate(year, month, day); + + uint64_t time = static_cast(julianDay * millisPerDay) + timestamp; + uint64_t tVMS = (time - julianOffset * millisPerDay) * 10000; // Convert to VMS format + + int d1 = static_cast(tVMS); + int d2 = static_cast(tVMS >> 32); + EncodeInt(d1, target); + EncodeInt(d2, static_cast(target) + 4); +} + +void +StripString(char * dest, const char * source, size_t length) +{ + char * dp = dest; + for (size_t i = 0; i < length && *source != '\0'; ++i) + { + *dp++ = *source++; + } + while (dp != dest && dp[-1] == ' ') + { + --dp; + } + *dp = '\0'; +} + +void +PadString(char * dest, const char * source, size_t length) +{ + size_t written = 0; + for (; written < length && *source != '\0'; ++written) + { + *dest++ = *source++; + } + for (; written < length; ++written) + { + *dest++ = ' '; + } +} diff --git a/Modules/IO/IOScanco/src/itkScancoHeaderIO.cxx b/Modules/IO/IOScanco/src/itkScancoHeaderIO.cxx new file mode 100644 index 00000000000..a2ed36ce254 --- /dev/null +++ b/Modules/IO/IOScanco/src/itkScancoHeaderIO.cxx @@ -0,0 +1,123 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ + +#include "itkScancoHeaderIO.h" +#include "itkScancoDataManipulation.h" +#include "itksys/SystemTools.hxx" +#include +#include + +namespace itk +{ +ScancoHeaderIO::ScancoHeaderIO(itkScancoHeaderData * headerData, std::string filename) + : m_HeaderData(headerData) + , m_FileName(filename) +{ + if (this->m_HeaderData == nullptr) + { + throw std::runtime_error("ScancoHeaderIO: headerData pointer is null."); + } +} + +unsigned long +ScancoHeaderIO::ReadHeader(std::ifstream & infile) +{ + throw std::runtime_error("ScancoHeaderIO::ReadHeader(std::ifstream&) not implemented."); +} + +unsigned long +ScancoHeaderIO::WriteHeader(std::ofstream & outfile, unsigned long imageSize) +{ + throw std::runtime_error("ScancoHeaderIO::WriteHeader(std::ofstream&) not implemented."); +} + +void +ScancoHeaderIO::SetFilename(const std::string filename) +{ + this->m_FileName = filename; +} + +void +ScancoHeaderIO::SetHeaderData(itkScancoHeaderData * headerData) +{ + this->m_HeaderData = headerData; +} + +unsigned long +ScancoHeaderIO::ReadHeader(const std::string filename) +{ + if (filename.empty() && this->m_FileName.empty()) + { + throw std::runtime_error("ScancoHeaderIO: No filename provided."); + } + if (!filename.empty()) + { + this->m_FileName = filename; + } + + std::ifstream infile; + +#ifdef _MSC_VER + const std::wstring uncpath = itksys::SystemTools::ConvertToWindowsExtendedPath(this->m_FileName.c_str()); + infile.open(uncpath.c_str(), std::ios::binary); +#else + infile.open(this->m_FileName.c_str(), std::ios::in | std::ios::binary); +#endif + + if (!infile.is_open() || infile.fail()) + { + throw std::runtime_error("Could not open file: " + this->m_FileName + + " for reading. Reason: " + std::strerror(errno) + "\n"); + } + return this->ReadHeader(infile); +} + +unsigned long +ScancoHeaderIO::WriteHeader(unsigned long imageSize, const std::string filename) +{ + if (filename.empty() && this->m_FileName.empty()) + { + throw std::runtime_error("ScancoHeaderWriter: No filename provided."); + } + else if (imageSize == 0) + { + throw std::runtime_error("ScancoHeaderWriter: No image bytes to write"); + } + if (!filename.empty()) + { + this->m_FileName = filename; + } + + std::ofstream outfile; + // Open the file for writing + + std::ios::openmode mode = std::ios::out | std::ios::trunc | std::ios::binary; + + outfile.open(this->m_FileName.c_str(), mode); + + if (!outfile.is_open() || outfile.fail()) + { + throw std::runtime_error("Could not open file: " + this->m_FileName + + " for writing. Reason: " + std::strerror(errno) + "\n"); + } + + return this->WriteHeader(outfile, imageSize); +} + + +} // end namespace itk diff --git a/Modules/IO/IOScanco/src/itkScancoImageIO.cxx b/Modules/IO/IOScanco/src/itkScancoImageIO.cxx new file mode 100644 index 00000000000..ad72d19cd14 --- /dev/null +++ b/Modules/IO/IOScanco/src/itkScancoImageIO.cxx @@ -0,0 +1,881 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ +/*========================================================================= + + Program: DICOM for VTK + + Copyright (c) 2015 David Gobbi + All rights reserved. + See Copyright.txt or http://dgobbi.github.io/bsd3.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + PURPOSE. See the above copyright notice for more information. + +=========================================================================*/ + +#include "itkScancoImageIO.h" +#include "itkSpatialOrientationAdapter.h" +#include "itkIOCommon.h" +#include "itksys/SystemTools.hxx" +#include "itkMath.h" +#include "itkIntTypes.h" +#include "itkByteSwapper.h" +#include "itkMetaDataObject.h" +#include "itkScancoDataManipulation.h" +#include "itkAIMHeaderIO.h" +#include "itkISQHeaderIO.h" + +#include +#include +#include +#include + +namespace itk +{ +ScancoImageIO::ScancoImageIO() + +{ + this->m_FileType = IOFileEnum::Binary; + this->m_ByteOrder = IOByteOrderEnum::LittleEndian; + + this->AddSupportedWriteExtension(".isq"); + this->AddSupportedWriteExtension(".aim"); + + this->AddSupportedReadExtension(".isq"); + this->AddSupportedReadExtension(".rsq"); + this->AddSupportedReadExtension(".rad"); + this->AddSupportedReadExtension(".aim"); + + this->m_HeaderData.m_RawHeader = nullptr; +} + + +ScancoImageIO::~ScancoImageIO() +{ + delete this->m_HeaderIO; + delete[] this->m_HeaderData.m_RawHeader; +} + + +void +ScancoImageIO::PrintSelf(std::ostream & os, Indent indent) const +{ + Superclass::PrintSelf(os, indent); +} + +bool +ScancoImageIO::CanReadFile(const char * filename) +{ + try + { + std::ifstream infile; + this->OpenFileForReading(infile, filename); + + bool canRead = false; + if (infile.good()) + { + // header is a 512 byte block + char buffer[512]; + infile.read(buffer, 512); + if (!infile.bad()) + { + int fileType = CheckVersion(buffer); + canRead = (fileType > 0); + } + } + + infile.close(); + + return canRead; + } + catch (...) // file cannot be opened, access denied etc. + { + return false; + } +} + + +void +ScancoImageIO::InitializeHeader() +{ + memset(this->m_HeaderData.m_Version, 0, 18); + memset(this->m_HeaderData.m_PatientName, 0, 42); + this->m_HeaderData.m_PatientIndex = 0; + this->m_HeaderData.m_ScannerID = 0; + memset(this->m_HeaderData.m_CreationDate, 0, 32); + memset(this->m_HeaderData.m_ModificationDate, 0, 32); + this->m_HeaderData.m_ScanDimensionsPixels[0] = 0; + this->m_HeaderData.m_ScanDimensionsPixels[1] = 0; + this->m_HeaderData.m_ScanDimensionsPixels[2] = 0; + this->m_HeaderData.m_ScanDimensionsPhysical[0] = 0; + this->m_HeaderData.m_ScanDimensionsPhysical[1] = 0; + this->m_HeaderData.m_ScanDimensionsPhysical[2] = 0; + this->m_HeaderData.m_SliceThickness = 0; + this->m_HeaderData.m_SliceIncrement = 0; + this->m_HeaderData.m_StartPosition = 0; + this->m_HeaderData.m_EndPosition = 0; + this->m_HeaderData.m_ZPosition = 0; + this->m_HeaderData.m_DataRange[0] = 0; + this->m_HeaderData.m_DataRange[1] = 0; + this->m_HeaderData.m_MuScaling = 1.0; + this->m_HeaderData.m_NumberOfSamples = 0; + this->m_HeaderData.m_NumberOfProjections = 0; + this->m_HeaderData.m_ScanDistance = 0; + this->m_HeaderData.m_SampleTime = 0; + this->m_HeaderData.m_ScannerType = 0; + this->m_HeaderData.m_MeasurementIndex = 0; + this->m_HeaderData.m_Site = 0; + this->m_HeaderData.m_ReconstructionAlg = 0; + this->m_HeaderData.m_ReferenceLine = 0; + this->m_HeaderData.m_Energy = 0; + this->m_HeaderData.m_Intensity = 0; + + this->m_HeaderData.m_RescaleType = 0; + memset(this->m_HeaderData.m_RescaleUnits, 0, 18); + memset(this->m_HeaderData.m_CalibrationData, 0, 66); + this->m_HeaderData.m_RescaleSlope = 1.0; + this->m_HeaderData.m_RescaleIntercept = 0.0; + this->m_HeaderData.m_MuWater = 0.70329999923706055; + + this->m_Compression = 0; + this->m_HeaderInitialized = true; +} + +void +ScancoImageIO::ParseAIMComponentType(int dataType) +{ + // a limited selection of data types are supported + // (only 0x00010001 (char) and 0x00020002 (short) are fully tested) + switch (dataType) + { + case 0x00160001: + this->SetComponentType(IOComponentEnum::UCHAR); + break; + case 0x000d0001: + this->SetComponentType(IOComponentEnum::UCHAR); + break; + case 0x00120003: + this->SetComponentType(IOComponentEnum::UCHAR); + this->SetPixelType(IOPixelEnum::VECTOR); + this->SetNumberOfDimensions(3); + break; + case 0x00010001: + this->SetComponentType(IOComponentEnum::CHAR); + break; + case 0x00060003: + this->SetComponentType(IOComponentEnum::CHAR); + this->SetPixelType(IOPixelEnum::VECTOR); + this->SetNumberOfDimensions(3); + break; + case 0x00170002: + this->SetComponentType(IOComponentEnum::USHORT); + break; + case 0x00020002: + this->SetComponentType(IOComponentEnum::SHORT); + break; + case 0x00030004: + this->SetComponentType(IOComponentEnum::INT); + break; + case 0x001a0004: + this->SetComponentType(IOComponentEnum::FLOAT); + break; + case 0x00150001: + this->m_Compression = 0x00b2; // run-length compressed bits + this->SetComponentType(IOComponentEnum::CHAR); + break; + case 0x00080002: + this->m_Compression = 0x00c2; // run-length compressed signed char + this->SetComponentType(IOComponentEnum::CHAR); + break; + case 0x00060001: + this->m_Compression = 0x00b1; // packed bits + this->SetComponentType(IOComponentEnum::CHAR); + break; + default: + itkExceptionMacro("Unrecognized data type in AIM file: " << dataType); + } +} + +void +ScancoImageIO::SetHeaderIO() +{ + std::string fileExtension = this->m_FileName.substr(this->m_FileName.find_last_of('.') + 1); + std::transform( + fileExtension.begin(), fileExtension.end(), fileExtension.begin(), [](unsigned char c) { return std::toupper(c); }); + + delete this->m_HeaderIO; + if (fileExtension == "AIM") + { + this->m_HeaderIO = new AIMHeaderIO(&this->m_HeaderData, this->m_FileName); + this->m_FileExtension = ScancoImageIO::ScancoFileExtensions::AIM; + } + else if (fileExtension == "RAD" || fileExtension == "ISQ" || fileExtension == "RSQ") + { + this->m_HeaderIO = new ISQHeaderIO(&this->m_HeaderData, this->m_FileName); + + if (fileExtension == "RAD") + { + this->m_FileExtension = ScancoImageIO::ScancoFileExtensions::RAD; + } + else if (fileExtension == "RSQ") + { + this->m_FileExtension = ScancoImageIO::ScancoFileExtensions::RSQ; + } + else + { + this->m_FileExtension = ScancoImageIO::ScancoFileExtensions::ISQ; + } + } + else + { + this->m_FileExtension = ScancoImageIO::ScancoFileExtensions::UNRECOGNIZED; + } +} + +void +ScancoImageIO::ReadImageInformation() +{ + this->InitializeHeader(); + + if (this->m_FileName.empty()) + { + itkExceptionMacro("FileName has not been set."); + } + + std::ifstream infile; + this->OpenFileForReading(infile, this->m_FileName); + + this->SetHeaderIO(); + if (this->m_FileExtension == ScancoImageIO::ScancoFileExtensions::UNRECOGNIZED) + { + infile.close(); + itkExceptionMacro("Incompatible filetype in: " << this->m_FileName); + } + + this->m_HeaderSize = this->m_HeaderIO->ReadHeader(infile); + + // set dimensions/spacing + this->SetNumberOfDimensions(3); + + for (unsigned int i = 0; i < m_NumberOfDimensions; ++i) + { + this->SetDimensions(i, this->m_HeaderData.m_PixelData.m_Dimensions[i]); + this->SetSpacing(i, this->m_HeaderData.m_PixelData.m_Spacing[i]); + // the origin will reflect the cropping of the data + this->SetOrigin(i, this->m_HeaderData.m_PixelData.m_Origin[i]); + } + + if (this->m_FileExtension == ScancoImageIO::ScancoFileExtensions::AIM) + { + this->ParseAIMComponentType(this->m_HeaderData.m_PixelData.m_ComponentType); + } + else + { + this->SetPixelType(IOPixelEnum::SCALAR); + this->SetComponentType(IOComponentEnum::SHORT); + this->m_Compression = 0; + } + + infile.close(); + this->PopulateMetaDataDictionary(); +} + +void +ScancoImageIO::PopulateMetaDataDictionary() +{ + std::vector dataRange(2); + std::vector pixelDimensions(3); + std::vector physicalDimensions(3); + MetaDataDictionary & thisDic = this->GetMetaDataDictionary(); + EncapsulateMetaData(thisDic, "Version", std::string(this->m_HeaderData.m_Version)); + EncapsulateMetaData(thisDic, "PatientName", std::string(this->m_HeaderData.m_PatientName)); + EncapsulateMetaData(thisDic, "PatientIndex", this->m_HeaderData.m_PatientIndex); + EncapsulateMetaData(thisDic, "ScannerID", this->m_HeaderData.m_ScannerID); + EncapsulateMetaData(thisDic, "CreationDate", std::string(this->m_HeaderData.m_CreationDate)); + EncapsulateMetaData(thisDic, "ModificationDate", std::string(this->m_HeaderData.m_ModificationDate)); + EncapsulateMetaData(thisDic, "SliceThickness", this->m_HeaderData.m_SliceThickness); + EncapsulateMetaData(thisDic, "SliceIncrement", this->m_HeaderData.m_SliceIncrement); + dataRange[0] = this->m_HeaderData.m_DataRange[0]; + dataRange[1] = this->m_HeaderData.m_DataRange[1]; + EncapsulateMetaData>(thisDic, "DataRange", dataRange); + EncapsulateMetaData(thisDic, "MuScaling", this->m_HeaderData.m_MuScaling); + EncapsulateMetaData(thisDic, "NumberOfSamples", this->m_HeaderData.m_NumberOfSamples); + EncapsulateMetaData(thisDic, "NumberOfProjections", this->m_HeaderData.m_NumberOfProjections); + EncapsulateMetaData(thisDic, "ScanDistance", this->m_HeaderData.m_ScanDistance); + EncapsulateMetaData(thisDic, "SampleTime", this->m_HeaderData.m_SampleTime); + EncapsulateMetaData(thisDic, "ScannerType", this->m_HeaderData.m_ScannerType); + EncapsulateMetaData(thisDic, "MeasurementIndex", this->m_HeaderData.m_MeasurementIndex); + EncapsulateMetaData(thisDic, "Site", this->m_HeaderData.m_Site); + EncapsulateMetaData(thisDic, "ReconstructionAlg", this->m_HeaderData.m_ReconstructionAlg); + EncapsulateMetaData(thisDic, "ReferenceLine", this->m_HeaderData.m_ReferenceLine); + EncapsulateMetaData(thisDic, "Energy", this->m_HeaderData.m_Energy); + EncapsulateMetaData(thisDic, "Intensity", this->m_HeaderData.m_Intensity); + EncapsulateMetaData(thisDic, "RescaleType", this->m_HeaderData.m_RescaleType); + EncapsulateMetaData(thisDic, "RescaleUnits", std::string(this->m_HeaderData.m_RescaleUnits)); + EncapsulateMetaData(thisDic, "CalibrationData", std::string(this->m_HeaderData.m_CalibrationData)); + EncapsulateMetaData(thisDic, "RescaleSlope", this->m_HeaderData.m_RescaleSlope); + EncapsulateMetaData(thisDic, "RescaleIntercept", this->m_HeaderData.m_RescaleIntercept); + EncapsulateMetaData(thisDic, "MuWater", this->m_HeaderData.m_MuWater); + EncapsulateMetaData(thisDic, "StartPosition", this->m_HeaderData.m_StartPosition); + + for (unsigned int i = 0; i < this->GetNumberOfDimensions(); i++) + { + pixelDimensions[i] = this->m_HeaderData.m_ScanDimensionsPixels[i]; + physicalDimensions[i] = this->m_HeaderData.m_ScanDimensionsPhysical[i]; + } + + EncapsulateMetaData>(thisDic, "PixelDimensions", pixelDimensions); + EncapsulateMetaData>(thisDic, "PhysicalDimensions", physicalDimensions); +} + +void +ScancoImageIO::SetHeaderFromMetaDataDictionary() +{ + std::vector pixelDimensions; + std::vector physicalDimensions; + MetaDataDictionary & metaData = this->GetMetaDataDictionary(); + + std::string stringMeta; + if (ExposeMetaData(metaData, "Version", stringMeta)) + { + std::snprintf(this->m_HeaderData.m_Version, sizeof(this->m_HeaderData.m_Version), "%s", stringMeta.c_str()); + } + if (ExposeMetaData(metaData, "PatientName", stringMeta)) + { + std::snprintf(this->m_HeaderData.m_PatientName, sizeof(this->m_HeaderData.m_PatientName), "%s", stringMeta.c_str()); + } + + ExposeMetaData(metaData, "PatientIndex", this->m_HeaderData.m_PatientIndex); + ExposeMetaData(metaData, "ScannerID", this->m_HeaderData.m_ScannerID); + + if (ExposeMetaData(metaData, "CreationDate", stringMeta)) + { + std::snprintf( + this->m_HeaderData.m_CreationDate, sizeof(this->m_HeaderData.m_CreationDate), "%s", stringMeta.c_str()); + } + if (ExposeMetaData(metaData, "ModificationDate", stringMeta)) + { + std::snprintf( + this->m_HeaderData.m_ModificationDate, sizeof(this->m_HeaderData.m_ModificationDate), "%s", stringMeta.c_str()); + } + + ExposeMetaData(metaData, "SliceThickness", this->m_HeaderData.m_SliceThickness); + ExposeMetaData(metaData, "SliceIncrement", this->m_HeaderData.m_SliceIncrement); + + std::vector dataRange; + if (ExposeMetaData>(metaData, "DataRange", dataRange) && dataRange.size() >= 2) + { + this->m_HeaderData.m_DataRange[0] = dataRange[0]; + this->m_HeaderData.m_DataRange[1] = dataRange[1]; + } + + ExposeMetaData(metaData, "MuScaling", this->m_HeaderData.m_MuScaling); + ExposeMetaData(metaData, "NumberOfSamples", this->m_HeaderData.m_NumberOfSamples); + ExposeMetaData(metaData, "NumberOfProjections", this->m_HeaderData.m_NumberOfProjections); + ExposeMetaData(metaData, "ScanDistance", this->m_HeaderData.m_ScanDistance); + ExposeMetaData(metaData, "SampleTime", this->m_HeaderData.m_SampleTime); + ExposeMetaData(metaData, "ScannerType", this->m_HeaderData.m_ScannerType); + ExposeMetaData(metaData, "MeasurementIndex", this->m_HeaderData.m_MeasurementIndex); + ExposeMetaData(metaData, "Site", this->m_HeaderData.m_Site); + ExposeMetaData(metaData, "ReconstructionAlg", this->m_HeaderData.m_ReconstructionAlg); + ExposeMetaData(metaData, "ReferenceLine", this->m_HeaderData.m_ReferenceLine); + ExposeMetaData(metaData, "Energy", this->m_HeaderData.m_Energy); + ExposeMetaData(metaData, "Intensity", this->m_HeaderData.m_Intensity); + + ExposeMetaData(metaData, "RescaleType", this->m_HeaderData.m_RescaleType); + if (ExposeMetaData(metaData, "RescaleUnits", stringMeta)) + { + std::snprintf( + this->m_HeaderData.m_RescaleUnits, sizeof(this->m_HeaderData.m_RescaleUnits), "%s", stringMeta.c_str()); + } + if (ExposeMetaData(metaData, "CalibrationData", stringMeta)) + { + std::snprintf( + this->m_HeaderData.m_CalibrationData, sizeof(this->m_HeaderData.m_CalibrationData), "%s", stringMeta.c_str()); + } + + ExposeMetaData(metaData, "RescaleSlope", this->m_HeaderData.m_RescaleSlope); + ExposeMetaData(metaData, "RescaleIntercept", this->m_HeaderData.m_RescaleIntercept); + ExposeMetaData(metaData, "MuWater", this->m_HeaderData.m_MuWater); + ExposeMetaData(metaData, "StartPosition", this->m_HeaderData.m_StartPosition); + + if (!ExposeMetaData>(metaData, "PixelDimensions", pixelDimensions) || + pixelDimensions.size() < this->GetNumberOfDimensions()) + { + return; + } + + if (!ExposeMetaData>(metaData, "PhysicalDimensions", physicalDimensions) || + physicalDimensions.size() < this->GetNumberOfDimensions()) + { + return; + } + + for (unsigned int i = 0; i < this->GetNumberOfDimensions(); i++) + { + this->m_HeaderData.m_ScanDimensionsPixels[i] = pixelDimensions[i]; + this->m_HeaderData.m_ScanDimensionsPhysical[i] = physicalDimensions[i]; + } +} + +template +void +ScancoImageIO::RescaleToHU(TBufferType * buffer, size_t size) +{ + double slope = this->m_HeaderData.m_RescaleSlope; + double intercept = this->m_HeaderData.m_RescaleIntercept; + + // This code causes rescaling to Hounsfield units + if (this->m_HeaderData.m_MuScaling > 1.0 && this->m_HeaderData.m_MuWater > 0) + { + // mu(voxel) = intensity(voxel) / m_MuScaling + // HU(voxel) = mu(voxel) * 1000/m_MuWater - 1000 + // Or, HU(voxel) = intensity(voxel) * (1000 / m_MuWater * m_MuScaling) - 1000 + slope = 1000.0 / (this->m_HeaderData.m_MuWater * this->m_HeaderData.m_MuScaling); + intercept = -1000.0; + + for (size_t i = 0; i < size; i++) + { + float bufferValue = static_cast(buffer[i]); + bufferValue = bufferValue * slope + intercept; + buffer[i] = static_cast(bufferValue); + } + } +} + +/** Rescale the image data to Scanco Units */ +template +void +ScancoImageIO::RescaleToScanco(TBufferType * buffer, size_t size) +{ + double slope = this->m_HeaderData.m_RescaleSlope; + double intercept = this->m_HeaderData.m_RescaleIntercept; + + // This code causes rescaling to Hounsfield units + if (this->m_HeaderData.m_MuScaling > 1.0 && this->m_HeaderData.m_MuWater > 0) + { + // mu(voxel) = intensity(voxel) / m_MuScaling + // HU(voxel) = mu(voxel) * 1000/m_MuWater - 1000 + // Or, HU(voxel) = intensity(voxel) * (1000 / m_MuWater * m_MuScaling) - 1000 + slope = 1000.0 / (this->m_HeaderData.m_MuWater * this->m_HeaderData.m_MuScaling); + intercept = -1000.0; + + for (size_t i = 0; i < size; i++) + { + float bufferValue = static_cast(buffer[i]); + bufferValue = (bufferValue - intercept) / slope; + buffer[i] = static_cast(bufferValue); + } + } +} + +void +ScancoImageIO::Read(void * buffer) +{ + std::ifstream infile; + this->OpenFileForReading(infile, this->m_FileName); + + // seek to the data + infile.seekg(this->m_HeaderSize); + + // get the size of the compressed data + int intSize = 4; + if (strcmp(this->m_HeaderData.m_Version, "AIMDATA_V030 ") == 0) + { + // header uses 64-bit ints (8 bytes) + intSize = 8; + } + + // Dimensions of the data + const int xsize = this->GetDimensions(0); + const int ysize = this->GetDimensions(1); + const int zsize = this->GetDimensions(2); + size_t outSize = this->GetImageSizeInBytes(); + + // For the input (compressed) data + char * input = nullptr; + size_t size = 0; + + if (this->m_Compression == 0) + { + infile.read(reinterpret_cast(buffer), outSize); + size = outSize; + } + else if (this->m_Compression == 0x00b1) + { + // Compute the size of the binary packed data + size_t xinc = (xsize + 1) / 2; + size_t yinc = (ysize + 1) / 2; + size_t zinc = (zsize + 1) / 2; + size = xinc * yinc * zinc + 1; + input = new char[size]; + infile.read(input, size); + } + else if (this->m_Compression == 0x00b2 || this->m_Compression == 0x00c2) + { + // Get the size of the compressed data + char head[8]; + infile.read(head, intSize); + size = static_cast(DecodeInt(head)); + if (intSize == 8) + { + // Read the high word of a 64-bit int + unsigned int high = DecodeInt(head + 4); + size += (static_cast(high) << 32); + } + input = new char[size - intSize]; + size -= intSize; + infile.read(input, size); + } + + // confirm that enough data was read + size_t shortread = size - infile.gcount(); + if (shortread != 0) + { + itkExceptionMacro("File is truncated, " << shortread << " bytes are missing"); + } + + // Close the file + infile.close(); + + auto * dataPtr = reinterpret_cast(buffer); + + if (this->m_Compression == 0x00b1) + { + // Unpack binary data, each byte becomes a 2x2x2 block of voxels + size_t xinc = (xsize + 1) / 2; + size_t yinc = (ysize + 1) / 2; + unsigned char v = input[size - 1]; + v = (v == 0 ? 0x7f : v); + unsigned char bit = 0; + for (int i = 0; i < zsize; i++) + { + bit ^= (bit & 2); + for (int j = 0; j < ysize; j++) + { + char * inPtr = input + (i * yinc + j) * xinc; + bit ^= (bit & 1); + for (int k = 0; k < xsize; k++) + { + unsigned char c = *inPtr; + *dataPtr++ = ((c >> bit) & 1) * v; + inPtr += (bit & 1); + bit ^= 1; + } + bit ^= 2; + } + bit ^= 4; + } + } + else if (this->m_Compression == 0x00b2) + { + // Decompress binary run-lengths + bool flip = false; + unsigned char v = input[flip]; + char * inPtr = input + 2; + size -= 2; + if (size > 0) + { + do + { + unsigned char l = *inPtr++; + if (l == 255) + { + l = 254; + flip = !flip; + } + if (l > outSize) + { + l = static_cast(outSize); + } + outSize -= l; + if (l > 0) + { + do + { + *dataPtr++ = v; + } while (--l); + } + flip = !flip; + v = input[flip]; + } while (--size != 0 && outSize != 0); + } + } + else if (this->m_Compression == 0x00c2) + { + // Decompress 8-bit run-lengths + char * inPtr = input; + size /= 2; + if (size > 0) + { + do + { + unsigned char l = inPtr[0]; + unsigned char v = inPtr[1]; + inPtr += 2; + if (l > outSize) + { + l = static_cast(outSize); + } + outSize -= l; + if (l > 0) + { + do + { + *dataPtr++ = v; + } while (--l); + } + } while (--size != 0 && outSize != 0); + } + } + + delete[] input; + + // Convert the image to HU. + // Only SHORT images have been tested + IOComponentEnum dataType = this->m_ComponentType; + + // The size of the buffer will change depending on the data type + size_t bufferSize = outSize; + + if (this->m_HeaderData.m_RescaleSlope != 1.0 || this->m_HeaderData.m_RescaleIntercept != 0.0) + { + switch (dataType) + { + case IOComponentEnum::CHAR: + RescaleToHU(reinterpret_cast(buffer), bufferSize); + break; + case IOComponentEnum::UCHAR: + RescaleToHU(reinterpret_cast(buffer), bufferSize); + break; + case IOComponentEnum::SHORT: + bufferSize /= 2; + RescaleToHU(reinterpret_cast(buffer), bufferSize); + break; + case IOComponentEnum::USHORT: + bufferSize /= 2; + RescaleToHU(reinterpret_cast(buffer), bufferSize); + break; + case IOComponentEnum::INT: + bufferSize /= 4; + RescaleToHU(reinterpret_cast(buffer), bufferSize); + break; + case IOComponentEnum::UINT: + bufferSize /= 4; + RescaleToHU(reinterpret_cast(buffer), bufferSize); + break; + case IOComponentEnum::FLOAT: + bufferSize /= 4; + RescaleToHU(reinterpret_cast(buffer), bufferSize); + break; + default: + itkExceptionMacro("Unrecognized data type in file: " << dataType); + } + } +} + + +bool +ScancoImageIO::CanWriteFile(const char * name) +{ + const std::string filename = name; + + if (filename.empty()) + { + return false; + } + + return this->HasSupportedWriteExtension(name, true); +} + +void +ScancoImageIO::SetDataTypeFromComponentEnum() +{ + if (this->m_ComponentType == IOComponentEnum::SHORT) + { + this->m_HeaderData.m_PixelData.m_ComponentType = 0x00020002; // short + } + else if (this->m_ComponentType == IOComponentEnum::FLOAT) + { + this->m_HeaderData.m_PixelData.m_ComponentType = 0x001a0004; // float + } + else if (this->m_ComponentType == IOComponentEnum::UCHAR) + { + this->m_HeaderData.m_PixelData.m_ComponentType = 0x00160001; // unsigned char + } + else if (this->m_ComponentType == IOComponentEnum::CHAR) + { + this->m_HeaderData.m_PixelData.m_ComponentType = 0x00010001; // char + } + else + { + itkExceptionMacro("ScancoImageIO only supports writing short, float, or unsigned char files."); + } +} + +void +ScancoImageIO::WriteImageInformation() +{ + if (this->m_FileName.empty()) + { + itkExceptionMacro("FileName has not been set."); + } + + std::ofstream outFile; + this->OpenFileForWriting(outFile, this->m_FileName); + + if (!this->m_HeaderInitialized) + { + this->InitializeHeader(); + } + this->SetHeaderFromMetaDataDictionary(); + + this->SetHeaderIO(); + + if (this->m_FileExtension == ScancoFileExtensions::UNRECOGNIZED) + { + itkExceptionMacro("ScancoImageIO: Cannot write file, incompatible extension type."); + } + else if (this->m_FileExtension == ScancoFileExtensions::AIM) + { + if (strcmp(this->m_HeaderData.m_Version, "AIMDATA_V020 ") == 0) + { + // writing to version 020 can be specified, but default is v030 + this->SetVersion("AIMDATA_V020 "); + } + else + { + this->SetVersion("AIMDATA_V030 "); + } + } + else + { + this->SetVersion("CTDATA-HEADER_V1"); + } + + this->SetDataTypeFromComponentEnum(); + + for (unsigned int i = 0; i < m_NumberOfDimensions; ++i) + { + this->m_HeaderData.m_PixelData.m_Dimensions[i] = this->GetDimensions(i); + this->m_HeaderData.m_PixelData.m_Spacing[i] = this->GetSpacing(i); + // the origin will reflect the cropping of the data + this->m_HeaderData.m_PixelData.m_Origin[i] = this->GetOrigin(i); + } + + this->m_HeaderSize = + static_cast(this->m_HeaderIO->WriteHeader(outFile, (unsigned long)this->GetImageSizeInBytes())); + + this->m_Compression = 0; + + outFile.close(); +} + + +void +ScancoImageIO::Write(const void * buffer) +{ + this->WriteImageInformation(); + + std::ofstream outFile; + this->OpenFileForWriting(outFile, this->m_FileName, false); + outFile.seekp(this->m_HeaderSize, std::ios::beg); + + const auto numberOfBytes = static_cast(this->GetImageSizeInBytes()); + const auto numberOfComponents = static_cast(this->GetImageSizeInComponents()); + + if (this->GetComponentType() != IOComponentEnum::SHORT && this->GetComponentType() != IOComponentEnum::FLOAT) + { + itkExceptionMacro("ScancoImageIO only supports writing short or float files."); + } + + std::vector tempmemory(numberOfBytes); + memcpy(tempmemory.data(), buffer, numberOfBytes); + + bool bigEndian = ByteSwapper::SystemIsBigEndian(); + + if (this->m_HeaderData.m_RescaleSlope != 1.0 || this->m_HeaderData.m_RescaleIntercept != 0.0) + { + switch (this->GetComponentType()) + { + case IOComponentEnum::CHAR: + RescaleToScanco(reinterpret_cast(tempmemory.data()), numberOfComponents); + if (bigEndian) + { + ByteSwapper::SwapRangeFromSystemToBigEndian(reinterpret_cast(tempmemory.data()), + numberOfComponents); + } + break; + case IOComponentEnum::UCHAR: + RescaleToScanco(reinterpret_cast(tempmemory.data()), numberOfComponents); + if (bigEndian) + { + ByteSwapper::SwapRangeFromSystemToBigEndian( + reinterpret_cast(tempmemory.data()), numberOfComponents); + } + break; + case IOComponentEnum::SHORT: + RescaleToScanco(reinterpret_cast(tempmemory.data()), numberOfComponents); + if (bigEndian) + { + ByteSwapper::SwapRangeFromSystemToBigEndian(reinterpret_cast(tempmemory.data()), + numberOfComponents); + } + break; + case IOComponentEnum::USHORT: + RescaleToScanco(reinterpret_cast(tempmemory.data()), numberOfComponents); + if (bigEndian) + { + ByteSwapper::SwapRangeFromSystemToBigEndian( + reinterpret_cast(tempmemory.data()), numberOfComponents); + } + break; + case IOComponentEnum::INT: + RescaleToScanco(reinterpret_cast(tempmemory.data()), numberOfComponents); + if (bigEndian) + { + ByteSwapper::SwapRangeFromSystemToBigEndian(reinterpret_cast(tempmemory.data()), + numberOfComponents); + } + break; + case IOComponentEnum::UINT: + RescaleToScanco(reinterpret_cast(tempmemory.data()), numberOfComponents); + if (bigEndian) + { + ByteSwapper::SwapRangeFromSystemToBigEndian(reinterpret_cast(tempmemory.data()), + numberOfComponents); + } + break; + case IOComponentEnum::FLOAT: + RescaleToScanco(reinterpret_cast(tempmemory.data()), numberOfComponents); + if (bigEndian) + { + ByteSwapper::SwapRangeFromSystemToBigEndian(reinterpret_cast(tempmemory.data()), + numberOfComponents); + } + break; + default: + itkExceptionMacro("Unrecognized data type in file: " << this->m_ComponentType); + } + } + outFile.write(static_cast(tempmemory.data()), numberOfBytes); + outFile.close(); +} + +} // end namespace itk diff --git a/Modules/IO/IOScanco/src/itkScancoImageIOFactory.cxx b/Modules/IO/IOScanco/src/itkScancoImageIOFactory.cxx new file mode 100644 index 00000000000..df5dda66f76 --- /dev/null +++ b/Modules/IO/IOScanco/src/itkScancoImageIOFactory.cxx @@ -0,0 +1,57 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ +#include "itkScancoImageIOFactory.h" +#include "itkScancoImageIO.h" +#include "itkVersion.h" + +namespace itk +{ +ScancoImageIOFactory::ScancoImageIOFactory() +{ + this->RegisterOverride( + "itkImageIOBase", "itkScancoImageIO", "Scanco Image IO", true, CreateObjectFunction::New()); +} + +const char * +ScancoImageIOFactory::GetITKSourceVersion() const +{ + return ITK_SOURCE_VERSION; +} + +const char * +ScancoImageIOFactory::GetDescription() const +{ + return "Scanco ImageIO Factory, allows the loading of Scanco images into insight"; +} + +// Undocumented API used to register during static initialization. +// DO NOT CALL DIRECTLY. + +static bool ScancoImageIOFactoryHasBeenRegistered; + +void IOScanco_EXPORT +ScancoImageIOFactoryRegister__Private() +{ + if (!ScancoImageIOFactoryHasBeenRegistered) + { + ScancoImageIOFactoryHasBeenRegistered = true; + ScancoImageIOFactory::RegisterOneFactory(); + } +} + +} // end namespace itk diff --git a/Modules/IO/IOScanco/test/Baseline/AIMIOTestImage.mha.cid b/Modules/IO/IOScanco/test/Baseline/AIMIOTestImage.mha.cid new file mode 100644 index 00000000000..2b38d1e901b --- /dev/null +++ b/Modules/IO/IOScanco/test/Baseline/AIMIOTestImage.mha.cid @@ -0,0 +1 @@ +bafkreifk44d637gtlkmfzfqg5mmebj5shloymqjlgok5xhpr6rumh74oya diff --git a/Modules/IO/IOScanco/test/Baseline/C0004255.mha.cid b/Modules/IO/IOScanco/test/Baseline/C0004255.mha.cid new file mode 100644 index 00000000000..f89f662d6b3 --- /dev/null +++ b/Modules/IO/IOScanco/test/Baseline/C0004255.mha.cid @@ -0,0 +1 @@ +bafkreifbfmols4ggdif45pusn6mpx6jzrnvj2hhzfqylsefrhow52l6orq diff --git a/Modules/IO/IOScanco/test/CMakeLists.txt b/Modules/IO/IOScanco/test/CMakeLists.txt new file mode 100644 index 00000000000..98d09767582 --- /dev/null +++ b/Modules/IO/IOScanco/test/CMakeLists.txt @@ -0,0 +1,196 @@ +itk_module_test() +set( + IOScancoTests + itkScancoImageIOTest.cxx + itkScancoImageIOTest2.cxx + itkScancoImageIOTest3.cxx +) + +createtestdriver(IOScanco "${IOScanco-Test_LIBRARIES}" "${IOScancoTests}") + +# C0004255.ISQ is a 107 MB input + 67 MB baseline fixture hosted on +# data.kitware.com via SHA512 (exceeds GitHub's 100 MB single-file limit so +# cannot live in ITKTestingData). Gate on ITK_COMPUTER_MEMORY_SIZE to match +# Modules/IO/TIFF/test/CMakeLists.txt itkLargeTIFFImageWriteReadTest* pattern. +if("${ITK_COMPUTER_MEMORY_SIZE}" GREATER 5) + itk_add_test( + NAME itkScancoImageIOISQConvertTest + COMMAND + IOScancoTestDriver + --compare + DATA{Baseline/C0004255.mha} + ${ITK_TEST_OUTPUT_DIR}/C0004255.mha + itkScancoImageIOTest + DATA{Input/C0004255.ISQ} + ${ITK_TEST_OUTPUT_DIR}/C0004255.mha + ) + set_tests_properties( + itkScancoImageIOISQConvertTest + PROPERTIES + LABELS + "BigIO;RUNS_LONG" + ) +endif() + +itk_add_test( + NAME itkScancoImageIOAIMConvertTest + COMMAND + IOScancoTestDriver + --compare + DATA{Baseline/AIMIOTestImage.mha} + ${ITK_TEST_OUTPUT_DIR}/AIMIOTestImage.mha + itkScancoImageIOTest + DATA{Input/AIMIOTestImage.AIM} + ${ITK_TEST_OUTPUT_DIR}/AIMIOTestImage.mha +) + +if("${ITK_COMPUTER_MEMORY_SIZE}" GREATER 5) + itk_add_test( + NAME itkScancoImageIOISQHeaderCheckTest + COMMAND + IOScancoTestDriver + itkScancoImageIOTest2 + DATA{Input/C0004255.ISQ} + ${ITK_TEST_OUTPUT_DIR}/C0003665.isq + 1 + ) + set_tests_properties( + itkScancoImageIOISQHeaderCheckTest + PROPERTIES + LABELS + "BigIO;RUNS_LONG" + ) +endif() + +itk_add_test( + NAME itkScancoImageIOAIMHeaderCheckTest + COMMAND + IOScancoTestDriver + itkScancoImageIOTest3 + DATA{Input/AIMIOTestImage.AIM} + ${ITK_TEST_OUTPUT_DIR}/AIMIOTestImage2.isq + 1 +) + +if("${ITK_COMPUTER_MEMORY_SIZE}" GREATER 5) + itk_add_test( + NAME itkScancoImageIOISQHeaderCheckTest2 + COMMAND + IOScancoTestDriver + --compare + DATA{Baseline/C0004255.mha} + ${ITK_TEST_OUTPUT_DIR}/C0004255Test2.mha + itkScancoImageIOTest2 + DATA{Input/C0004255.ISQ} + ${ITK_TEST_OUTPUT_DIR}/C0004255Test2.mha + ) + + itk_add_test( + NAME itkScancoImageIOISQHeaderWriteTest2 + COMMAND + IOScancoTestDriver + --compare + DATA{Baseline/C0004255.mha} + ${ITK_TEST_OUTPUT_DIR}/C0004255Test2.isq + itkScancoImageIOTest2 + DATA{Input/C0004255.ISQ} + ${ITK_TEST_OUTPUT_DIR}/C0004255Test2.isq + ) + + set_tests_properties( + itkScancoImageIOISQHeaderCheckTest2 + itkScancoImageIOISQHeaderWriteTest2 + PROPERTIES + LABELS + "BigIO;RUNS_LONG" + ) +endif() + +itk_add_test( + NAME itkScancoImageIOAIMHeaderCheckTest2 + COMMAND + IOScancoTestDriver + --compare + DATA{Baseline/AIMIOTestImage.mha} + ${ITK_TEST_OUTPUT_DIR}/AIMIOTestImageTest3.mha + itkScancoImageIOTest3 + DATA{Input/AIMIOTestImage.AIM} + ${ITK_TEST_OUTPUT_DIR}/AIMIOTestImageTest3.mha +) +set_property( + TEST + itkScancoImageIOAIMHeaderCheckTest2 + APPEND + PROPERTY + DEPENDS + itkScancoImageIOAIMHeaderCheckTest +) + +if("${ITK_COMPUTER_MEMORY_SIZE}" GREATER 5) + itk_add_test( + NAME itkScancoImageIOISQReadWritten + COMMAND + IOScancoTestDriver + itkScancoImageIOTest2 + ${ITK_TEST_OUTPUT_DIR}/C0004255Test2.isq + ${ITK_TEST_OUTPUT_DIR}/ISQWrite.ISQ + ) + set_property( + TEST + itkScancoImageIOISQReadWritten + APPEND + PROPERTY + DEPENDS + itkScancoImageIOISQHeaderWriteTest2 + ) + set_tests_properties( + itkScancoImageIOISQReadWritten + PROPERTIES + LABELS + "BigIO;RUNS_LONG" + ) +endif() + +itk_add_test( + NAME itkScancoImageIOAIMWriteTest + COMMAND + IOScancoTestDriver + --compare + DATA{Baseline/AIMIOTestImage.mha} + ${ITK_TEST_OUTPUT_DIR}/AIMIOTestWrite.aim + itkScancoImageIOTest3 + DATA{Input/AIMIOTestImage.AIM} + ${ITK_TEST_OUTPUT_DIR}/AIMIOTestWrite.aim +) + +itk_add_test( + NAME itkScancoImageIOAIMReadWrittenTest + COMMAND + IOScancoTestDriver + itkScancoImageIOTest3 + ${ITK_TEST_OUTPUT_DIR}/AIMIOTestWrite.aim + ${ITK_TEST_OUTPUT_DIR}/AIMIOTestWrite2.aim +) + +set_property( + TEST + itkScancoImageIOAIMReadWrittenTest + APPEND + PROPERTY + DEPENDS + itkScancoImageIOAIMWriteTest +) + +itk_add_test( + NAME itkScancoImageIOAIM030FloatReadHeader + COMMAND + IOScancoTestDriver + --compare + DATA{Input/AIMIOTestImageFloatv030.AIM} + ${ITK_TEST_OUTPUT_DIR}/AIMIOTestWritev030.aim + itkScancoImageIOTest3 + DATA{Input/AIMIOTestImageFloatv030.AIM} + ${ITK_TEST_OUTPUT_DIR}/AIMIOTestWritev030.aim + 0 + "AIMDATA_V030 " +) diff --git a/Modules/IO/IOScanco/test/Input/AIMIOTestImage.AIM.cid b/Modules/IO/IOScanco/test/Input/AIMIOTestImage.AIM.cid new file mode 100644 index 00000000000..b324560cdda --- /dev/null +++ b/Modules/IO/IOScanco/test/Input/AIMIOTestImage.AIM.cid @@ -0,0 +1 @@ +bafkreiaqiishp2jgb5is5gjx4gmbvnskjed5bzejuthbu2zutaf7xmaehe diff --git a/Modules/IO/IOScanco/test/Input/AIMIOTestImageFloatv030.AIM.cid b/Modules/IO/IOScanco/test/Input/AIMIOTestImageFloatv030.AIM.cid new file mode 100644 index 00000000000..269288837ba --- /dev/null +++ b/Modules/IO/IOScanco/test/Input/AIMIOTestImageFloatv030.AIM.cid @@ -0,0 +1 @@ +bafybeictniw26bvuenhwloieng44sq2nuzlmkw5pajagudnbf6dken7b6a diff --git a/Modules/IO/IOScanco/test/Input/C0004255.ISQ.sha512 b/Modules/IO/IOScanco/test/Input/C0004255.ISQ.sha512 new file mode 100644 index 00000000000..3227872e0a0 --- /dev/null +++ b/Modules/IO/IOScanco/test/Input/C0004255.ISQ.sha512 @@ -0,0 +1 @@ +9101d92b0726e693b06ced64613ebc93b33bcda80b73bc83ed23efcbe7c2d7adc09b6f1c02e0ee03eaef0f28026041924035b22ff54d809afc5a29242999c654 diff --git a/Modules/IO/IOScanco/test/itkScancoImageIOTest.cxx b/Modules/IO/IOScanco/test/itkScancoImageIOTest.cxx new file mode 100644 index 00000000000..ba9e36b570d --- /dev/null +++ b/Modules/IO/IOScanco/test/itkScancoImageIOTest.cxx @@ -0,0 +1,105 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ + +#include +#include "itkImageFileReader.h" +#include "itkImageFileWriter.h" +#include "itkScancoImageIO.h" +#include "itkTestingMacros.h" + + +#define SPECIFIC_IMAGEIO_MODULE_TEST + +int +itkScancoImageIOTest(int argc, char * argv[]) +{ + if (argc < 3) + { + std::cerr << "Missing parameters." << std::endl; + std::cerr << "Usage: " << std::endl; + std::cerr << itkNameOfTestExecutableMacro(argv) << " Input Output" << std::endl; + return EXIT_FAILURE; + } + const char * inputFileName = argv[1]; + const char * outputFileName = argv[2]; + bool expectFailure = false; + if (argc > 3) + { + expectFailure = true; + std::cout << "Expecting failure for this test." << std::endl; + } + + // ATTENTION THIS IS THE PIXEL TYPE FOR + // THE RESULTING IMAGE + constexpr unsigned int Dimension = 3; + using PixelType = short; + using ImageType = itk::Image; + + using ReaderType = itk::ImageFileReader; + ReaderType::Pointer reader = ReaderType::New(); + + // force use of ScancoIO + using IOType = itk::ScancoImageIO; + IOType::Pointer scancoIO = IOType::New(); + + ITK_EXERCISE_BASIC_OBJECT_METHODS(scancoIO, ScancoImageIO, ImageIOBase); + + + reader->SetImageIO(scancoIO); + + // check usability of dimension (for coverage) + if (!scancoIO->SupportsDimension(3)) + { + std::cerr << "Did not support dimension 3" << std::endl; + return EXIT_FAILURE; + } + + // read the file + reader->SetFileName(inputFileName); + try + { + reader->Update(); + } + catch (itk::ExceptionObject & error) + { + std::cerr << "Exception in the file reader " << std::endl; + std::cerr << error << std::endl; + if (expectFailure) + { + return EXIT_SUCCESS; + } + return EXIT_FAILURE; + } + + ImageType::Pointer image = reader->GetOutput(); + image->Print(std::cout); + + ImageType::RegionType region = image->GetLargestPossibleRegion(); + std::cout << "region " << region; + + // Generate test image + using WriterType = itk::ImageFileWriter; + WriterType::Pointer writer = WriterType::New(); + writer->SetInput(reader->GetOutput()); + writer->SetFileName(outputFileName); + ITK_TRY_EXPECT_NO_EXCEPTION(writer->Update()); + + + std::cout << "Test finished" << std::endl; + return EXIT_SUCCESS; +} diff --git a/Modules/IO/IOScanco/test/itkScancoImageIOTest2.cxx b/Modules/IO/IOScanco/test/itkScancoImageIOTest2.cxx new file mode 100644 index 00000000000..a484e0b9f85 --- /dev/null +++ b/Modules/IO/IOScanco/test/itkScancoImageIOTest2.cxx @@ -0,0 +1,194 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ + +#include +#include "itkImageFileReader.h" +#include "itkImageFileWriter.h" +#include "itkScancoImageIO.h" +#include "itkScancoImageIOFactory.h" +#include "itkTestingMacros.h" + + +#define SPECIFIC_IMAGEIO_MODULE_TEST + +int +itkScancoImageIOTest2(int argc, char * argv[]) +{ + if (argc < 3) + { + std::cerr << "Missing parameters." << std::endl; + std::cerr << "Usage: " << std::endl; + std::cerr << itkNameOfTestExecutableMacro(argv) << " Input Output [ReUseIO]" << std::endl; + return EXIT_FAILURE; + } + const std::string inputFileName = argv[1]; + const std::string outputFileName = argv[2]; + + // ATTENTION THIS IS THE PIXEL TYPE FOR + // THE RESULTING IMAGE + constexpr unsigned int Dimension = 3; + using PixelType = short; + using ImageType = itk::Image; + ImageType::Pointer image; + + using ReaderType = itk::ImageFileReader; + ReaderType::Pointer reader = ReaderType::New(); + + itk::ScancoImageIOFactory::RegisterOneFactory(); + using IOType = itk::ScancoImageIO; + IOType::Pointer scancoIO = IOType::New(); + + ITK_EXERCISE_BASIC_OBJECT_METHODS(scancoIO, ScancoImageIO, ImageIOBase); + + + reader->SetImageIO(scancoIO); + reader->SetFileName(inputFileName); + // Populate the IO with file's metadata + ITK_TRY_EXPECT_NO_EXCEPTION(reader->UpdateOutputInformation()); + // Read the file without explicitly requesting ScancoIO + ITK_TRY_EXPECT_NO_EXCEPTION(image = itk::ReadImage(inputFileName)); + itk::MetaDataDictionary & metaData = image->GetMetaDataDictionary(); // Get metadata from regularly read image + + std::cout << "Version: \t\t" << scancoIO->GetVersion() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetVersion(), std::string("CTDATA-HEADER_V1")); + std::string stringMeta; + itk::ExposeMetaData(metaData, "Version", stringMeta); + ITK_TEST_EXPECT_EQUAL(stringMeta, std::string("CTDATA-HEADER_V1")); + std::cout << "PatientIndex: \t" << scancoIO->GetPatientIndex() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetPatientIndex(), 78); + int intMeta; + itk::ExposeMetaData(metaData, "PatientIndex", intMeta); + ITK_TEST_EXPECT_EQUAL(intMeta, 78); + std::cout << "ScannerID: \t\t" << scancoIO->GetScannerID() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetScannerID(), 2135); + itk::ExposeMetaData(metaData, "ScannerID", intMeta); + ITK_TEST_EXPECT_EQUAL(intMeta, 2135); + std::cout << "SliceThickness: \t" << scancoIO->GetSliceThickness() << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetSliceThickness(), 0.036, 6, 1e-3)); + double doubleMeta; + itk::ExposeMetaData(metaData, "SliceThickness", doubleMeta); + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(doubleMeta, 0.036, 6, 1e-3)); + std::cout << "SliceIncrement: \t" << scancoIO->GetSliceIncrement() << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetSliceIncrement(), 0.036, 6, 1e-3)); + itk::ExposeMetaData(metaData, "SliceIncrement", doubleMeta); + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(doubleMeta, 0.036, 6, 1e-3)); + std::cout << "StartPosition: \t" << scancoIO->GetStartPosition() << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetStartPosition(), 75.0, 6, 1e-3)); + std::vector vectorDoubleMeta; + itk::ExposeMetaData>(metaData, "DataRange", vectorDoubleMeta); + std::cout << "DataRange[0]: \t" << scancoIO->GetDataRange()[0] << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetDataRange()[0], -2813.0, 6, 1e-3)); + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(vectorDoubleMeta[0], -2813.0, 6, 1e-3)); + std::cout << "DataRange[1]: \t" << scancoIO->GetDataRange()[1] << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetDataRange()[1], 32767.0, 6, 1e-3)); + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(vectorDoubleMeta[1], 32767.0, 6, 1e-3)); + std::cout << "MuScaling: \t\t" << scancoIO->GetMuScaling() << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetMuScaling(), 4096.0, 6, 1e-3)); + itk::ExposeMetaData(metaData, "MuScaling", doubleMeta); + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(doubleMeta, 4096.0, 6, 1e-3)); + std::cout << "MuWater: \t\t" << scancoIO->GetMuWater() << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetMuWater(), 0.7033, 6, 1e-3)); + itk::ExposeMetaData(metaData, "MuWater", doubleMeta); + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(doubleMeta, 0.7033, 6, 1e-3)); + std::cout << "RescaleType: \t\t" << scancoIO->GetRescaleType() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetRescaleType(), 2); + itk::ExposeMetaData(metaData, "RescaleType", intMeta); + ITK_TEST_EXPECT_EQUAL(intMeta, 2); + std::cout << "RescaleSlope: \t\t" << scancoIO->GetRescaleSlope() << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetRescaleSlope(), 165.9970, 6, 1e-3)); + itk::ExposeMetaData(metaData, "RescaleSlope", doubleMeta); + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(doubleMeta, 165.9970, 6, 1e-3)); + std::cout << "RescaleIntercept: \t\t" << scancoIO->GetRescaleIntercept() << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetRescaleIntercept(), -129.6810, 6, 1e-3)); + itk::ExposeMetaData(metaData, "RescaleIntercept", doubleMeta); + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(doubleMeta, -129.6810, 6, 1e-3)); + std::cout << "RescaleUnits: \t\t" << scancoIO->GetRescaleUnits() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetRescaleUnits(), std::string("mg HA/ccm")); + itk::ExposeMetaData(metaData, "RescaleUnits", stringMeta); + ITK_TEST_EXPECT_EQUAL(stringMeta, std::string("mg HA/ccm")); + std::cout << "CalibrationData: \t\t" << scancoIO->GetCalibrationData() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetCalibrationData(), + std::string("45 kVp, 0.5mm Al, BH: 1200mg HA/ccm, Scaling 4096")); + itk::ExposeMetaData(metaData, "CalibrationData", stringMeta); + ITK_TEST_EXPECT_EQUAL(stringMeta, std::string("45 kVp, 0.5mm Al, BH: 1200mg HA/ccm, Scaling 4096")); + std::cout << "NumberOfSamples: \t" << scancoIO->GetNumberOfSamples() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetNumberOfSamples(), 1024); + itk::ExposeMetaData(metaData, "NumberOfSamples", intMeta); + ITK_TEST_EXPECT_EQUAL(intMeta, 1024); + std::cout << "NumberOfProjections: " << scancoIO->GetNumberOfProjections() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetNumberOfProjections(), 500); + itk::ExposeMetaData(metaData, "NumberOfProjections", intMeta); + ITK_TEST_EXPECT_EQUAL(intMeta, 500); + std::cout << "ScanDistance: \t" << scancoIO->GetScanDistance() << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetScanDistance(), 36.864, 6, 1e-3)); + itk::ExposeMetaData(metaData, "ScanDistance", doubleMeta); + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(doubleMeta, 36.864, 6, 1e-3)); + std::cout << "ScannerType: \t" << scancoIO->GetScannerType() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetScannerType(), 10); + itk::ExposeMetaData(metaData, "ScannerType", intMeta); + ITK_TEST_EXPECT_EQUAL(intMeta, 10); + std::cout << "SampleTime: \t\t" << scancoIO->GetSampleTime() << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetSampleTime(), 400.0, 6, 1e-3)); + itk::ExposeMetaData(metaData, "SampleTime", doubleMeta); + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(doubleMeta, 400.0, 6, 1e-3)); + std::cout << "MeasurementIndex: \t" << scancoIO->GetMeasurementIndex() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetMeasurementIndex(), 4937); + itk::ExposeMetaData(metaData, "MeasurementIndex", intMeta); + ITK_TEST_EXPECT_EQUAL(intMeta, 4937); + std::cout << "Site: \t\t" << scancoIO->GetSite() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetSite(), 5); + itk::ExposeMetaData(metaData, "Site", intMeta); + ITK_TEST_EXPECT_EQUAL(intMeta, 5); + std::cout << "ReferenceLine: \t" << scancoIO->GetReferenceLine() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetReferenceLine(), 0); + itk::ExposeMetaData(metaData, "ReferenceLine", doubleMeta); + ITK_TEST_EXPECT_EQUAL(doubleMeta, 0); + std::cout << "ReconstructionAlg: \t" << scancoIO->GetReconstructionAlg() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetReconstructionAlg(), 3); + itk::ExposeMetaData(metaData, "ReconstructionAlg", intMeta); + ITK_TEST_EXPECT_EQUAL(intMeta, 3); + std::cout << "Energy: \t\t" << scancoIO->GetEnergy() << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetEnergy(), 45.0, 6, 1e-3)); + itk::ExposeMetaData(metaData, "Energy", doubleMeta); + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(doubleMeta, 45.0, 6, 1e-3)); + std::cout << "Intensity: \t\t" << scancoIO->GetIntensity() << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetIntensity(), 0.177, 6, 1e-3)); + itk::ExposeMetaData(metaData, "Intensity", doubleMeta); + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(doubleMeta, 0.177, 6, 1e-3)); + std::cout << "CreationDate: \t" << scancoIO->GetCreationDate() << std::endl; + std::cout << "ModificationDate: \t" << scancoIO->GetModificationDate() << std::endl; + + itk::EncapsulateMetaData(metaData, "PatientName", std::string("Zukic")); + + using WriterType = itk::ImageFileWriter; + WriterType::Pointer writer = WriterType::New(); + if (argc > 3) // Explicitly use scancoIO + { + ITK_TEST_EXPECT_TRUE(scancoIO->CanWriteFile(outputFileName.c_str())); + + ITK_TEST_EXPECT_TRUE(!scancoIO->CanWriteFile((outputFileName + ".exe").c_str())); + writer->SetImageIO(scancoIO); + } + writer->SetInput(image); + writer->SetFileName(outputFileName); + ITK_TRY_EXPECT_NO_EXCEPTION(writer->Update()); + + + std::cout << "Test finished" << std::endl; + return EXIT_SUCCESS; +} diff --git a/Modules/IO/IOScanco/test/itkScancoImageIOTest3.cxx b/Modules/IO/IOScanco/test/itkScancoImageIOTest3.cxx new file mode 100644 index 00000000000..68e6d17712c --- /dev/null +++ b/Modules/IO/IOScanco/test/itkScancoImageIOTest3.cxx @@ -0,0 +1,152 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ + +#include +#include "itkImageFileReader.h" +#include "itkImageFileWriter.h" +#include "itkScancoImageIO.h" +#include "itkScancoImageIOFactory.h" +#include "itkTestingMacros.h" + + +#define SPECIFIC_IMAGEIO_MODULE_TEST + +int +itkScancoImageIOTest3(int argc, char * argv[]) +{ + if (argc < 3) + { + std::cerr << "Missing parameters." << std::endl; + std::cerr << "Usage: " << std::endl; + std::cerr << itkNameOfTestExecutableMacro(argv) << " Input Output [ReUseIO]" << std::endl; + return EXIT_FAILURE; + } + const std::string inputFileName = argv[1]; + const std::string outputFileName = argv[2]; + const std::string versionString = (argc > 4) ? argv[4] : "AIMDATA_V020 "; + + // ATTENTION THIS IS THE PIXEL TYPE FOR + // THE RESULTING IMAGE + constexpr unsigned int Dimension = 3; + using PixelType = short; + using ImageType = itk::Image; + + using ReaderType = itk::ImageFileReader; + ReaderType::Pointer reader = ReaderType::New(); + + // force use of ScancoIO + using IOType = itk::ScancoImageIO; + IOType::Pointer scancoIO = IOType::New(); + + itk::ScancoImageIOFactory::RegisterOneFactory(); + + ITK_EXERCISE_BASIC_OBJECT_METHODS(scancoIO, ScancoImageIO, ImageIOBase); + + + reader->SetImageIO(scancoIO); + + // read the file + reader->SetFileName(inputFileName); + ITK_TRY_EXPECT_NO_EXCEPTION(reader->Update()); + ImageType::Pointer image = reader->GetOutput(); + + std::cout << "Version: \t\t" << scancoIO->GetVersion() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetVersion(), versionString); + std::cout << "PatientIndex: \t" << scancoIO->GetPatientIndex() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetPatientIndex(), 2573); + std::cout << "ScannerID: \t\t" << scancoIO->GetScannerID() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetScannerID(), 3401); + std::cout << "SliceThickness: \t" << scancoIO->GetSliceThickness() << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetSliceThickness(), 0.0607, 6, 1e-3)); + std::cout << "SliceIncrement: \t" << scancoIO->GetSliceIncrement() << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetSliceIncrement(), 0.0607, 6, 1e-3)); + std::cout << "StartPosition: \t" << scancoIO->GetStartPosition() << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetStartPosition(), 114.845, 6, 1e-3)); + std::cout << "DataRange[0]: \t" << scancoIO->GetDataRange()[0] << std::endl; + std::cout << "DataRange[1]: \t" << scancoIO->GetDataRange()[1] << std::endl; + + if (scancoIO->GetComponentType() == itk::IOComponentEnum::FLOAT) + { + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetDataRange()[0], -1380.0, 6, 1e-3)); + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetDataRange()[1], 8823.0, 6, 1e-3)); + } + else + { + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetDataRange()[0], -2478.0, 6, 1e-3)); + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetDataRange()[1], 11662.0, 6, 1e-3)); + } + + std::cout << "MuScaling: \t\t" << scancoIO->GetMuScaling() << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetMuScaling(), 8192.0, 6, 1e-3)); + std::cout << "MuWater: \t\t" << scancoIO->GetMuWater() << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetMuWater(), 0.24090, 6, 1e-3)); + std::cout << "RescaleType: \t\t" << scancoIO->GetRescaleType() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetRescaleType(), 2); + std::cout << "RescaleSlope: \t\t" << scancoIO->GetRescaleSlope() << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetRescaleSlope(), 1603.51904, 6, 1e-3)); + std::cout << "RescaleIntercept: \t\t" << scancoIO->GetRescaleIntercept() << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetRescaleIntercept(), -391.209015, 6, 1e-3)); + std::cout << "RescaleUnits: \t\t" << scancoIO->GetRescaleUnits() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetRescaleUnits(), std::string("mg HA/ccm")); + std::cout << "CalibrationData: \t\t" << scancoIO->GetCalibrationData() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetCalibrationData(), std::string("68 kVp, BH: 200 mg HA/ccm, Scaling 8192, 0.2 CU")); + std::cout << "NumberOfSamples: \t" << scancoIO->GetNumberOfSamples() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetNumberOfSamples(), 2304); + std::cout << "NumberOfProjections: " << scancoIO->GetNumberOfProjections() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetNumberOfProjections(), 900); + std::cout << "ScanDistance: \t" << scancoIO->GetScanDistance() << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetScanDistance(), 139.852, 6, 1e-3)); + std::cout << "ScannerType: \t" << scancoIO->GetScannerType() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetScannerType(), 9); + std::cout << "SampleTime: \t\t" << scancoIO->GetSampleTime() << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetSampleTime(), 43.0, 6, 1e-3)); + std::cout << "MeasurementIndex: \t" << scancoIO->GetMeasurementIndex() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetMeasurementIndex(), 12839); + std::cout << "Site: \t\t" << scancoIO->GetSite() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetSite(), 21); + std::cout << "ReferenceLine: \t" << scancoIO->GetReferenceLine() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetReferenceLine(), 105); + std::cout << "ReconstructionAlg: \t" << scancoIO->GetReconstructionAlg() << std::endl; + ITK_TEST_EXPECT_EQUAL(scancoIO->GetReconstructionAlg(), 3); + std::cout << "Energy: \t\t" << scancoIO->GetEnergy() << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetEnergy(), 68.0, 6, 1e-3)); + std::cout << "Intensity: \t\t" << scancoIO->GetIntensity() << std::endl; + ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(scancoIO->GetIntensity(), 1.470, 6, 1e-3)); + std::cout << "CreationDate: \t" << scancoIO->GetCreationDate() << std::endl; + std::cout << "ModificationDate: \t" << scancoIO->GetModificationDate() << std::endl; + + // Generate test image + using WriterType = itk::ImageFileWriter; + WriterType::Pointer writer = WriterType::New(); + if (argc > 3 && std::stoi(argv[3]) == 1) // Explicitly use scancoIO + { + ITK_TEST_EXPECT_TRUE(scancoIO->CanWriteFile(outputFileName.c_str())); + + ITK_TEST_EXPECT_TRUE(!scancoIO->CanWriteFile((outputFileName + ".exe").c_str())); + + writer->SetImageIO(scancoIO); + scancoIO->SetPatientName("Kuczynski"); + } + writer->SetInput(reader->GetOutput()); + writer->SetFileName(outputFileName); + ITK_TRY_EXPECT_NO_EXCEPTION(writer->Update()); + + + std::cout << "Test finished" << std::endl; + return EXIT_SUCCESS; +} diff --git a/Modules/IO/IOScanco/wrapping/CMakeLists.txt b/Modules/IO/IOScanco/wrapping/CMakeLists.txt new file mode 100644 index 00000000000..93f2627e337 --- /dev/null +++ b/Modules/IO/IOScanco/wrapping/CMakeLists.txt @@ -0,0 +1,3 @@ +itk_wrap_module(IOScanco) +itk_auto_load_submodules() +itk_end_wrap_module() diff --git a/Modules/IO/IOScanco/wrapping/itkScancoImageIO.wrap b/Modules/IO/IOScanco/wrapping/itkScancoImageIO.wrap new file mode 100644 index 00000000000..412e14fdfd9 --- /dev/null +++ b/Modules/IO/IOScanco/wrapping/itkScancoImageIO.wrap @@ -0,0 +1,2 @@ +itk_wrap_simple_class("itk::ScancoImageIO" POINTER) +itk_wrap_simple_class("itk::ScancoImageIOFactory" POINTER) diff --git a/Modules/Remote/IOScanco.remote.cmake b/Modules/Remote/IOScanco.remote.cmake deleted file mode 100644 index 4791d9936b4..00000000000 --- a/Modules/Remote/IOScanco.remote.cmake +++ /dev/null @@ -1,50 +0,0 @@ -#-- # Grading Level Criteria Report -#-- EVALUATION DATE: 2020-03-01 -#-- EVALUATORS: [<>,<>] -#-- -#-- ## Compliance level 5 star (AKA ITK main modules, or remote modules that could become core modules) -#-- - [ ] Widespread community dependance -#-- - [ ] Above 90% code coverage -#-- - [ ] CI dashboards and testing monitored rigorously -#-- - [ ] Key API features are exposed in wrapping interface -#-- - [ ] All requirements of Levels 4,3,2,1 -#-- -#-- ## Compliance Level 4 star (Very high-quality code, perhaps small community dependance) -#-- - [ ] Meets all ITK code style standards -#-- - [ ] No external requirements beyond those needed by ITK proper -#-- - [ ] Builds and passes tests on all supported platforms within 1 month of each core tagged release -#-- - [ ] Windows Shared Library Build with Visual Studio -#-- - [ ] Mac with clang compiller -#-- - [ ] Linux with gcc compiler -#-- - [ ] Active developer community dedicated to maintaining code-base -#-- - [ ] 75% code coverage demonstrated for testing suite -#-- - [ ] Continuous integration testing performed -#-- - [ ] All requirements of Levels 3,2,1 -#-- -#-- ## Compliance Level 3 star (Quality beta code) -#-- - [ ] API | executable interface is considered mostly stable and feature complete -#-- - [ ] 10% C0-code coverage demonstrated for testing suite -#-- - [x] Some tests exist and pass on at least some platform -#-- - [X] All requirements of Levels 2,1 -#-- -#-- ## Compliance Level 2 star (Alpha code feature API development or niche community/execution environment dependance ) -#-- - [X] Compiles for at least 1 niche set of execution envirionments, and perhaps others -#-- (may depend on specific external tools like a java environment, or specific external libraries to work ) -#-- - [X] All requirements of Levels 1 -#-- -#-- ## Compliance Level 1 star (Pre-alpha features under development and code of unknown quality) -#-- - [X] Code complies on at least 1 platform -#-- -#-- ## Compliance Level 0 star ( Code/Feature of known poor-quality or deprecated status ) -#-- - [ ] Code reviewed and explicitly identified as not recommended for use -#-- -#-- ### Please document here any justification for the criteria above -# Code style enforced by clang-format on 2020-02-19, and clang-tidy modernizations completed - -itk_fetch_module( - IOScanco - "An ITK module to read and write Scanco microCT .isq files." - MODULE_COMPLIANCE_LEVEL 2 - GIT_REPOSITORY https://github.com/InsightSoftwareConsortium/ITKIOScanco.git - GIT_TAG 2209183eac3d4c62de8fb5c8f42e4ac86e634c83 - ) diff --git a/pyproject.toml b/pyproject.toml index 0c4a1bed639..2a30796a550 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,27 +55,25 @@ cmd = '''cmake \ -DModule_AdaptiveDenoising:BOOL=ON \ -DModule_AnisotropicDiffusionLBR:BOOL=ON \ -DModule_Cuberille:BOOL=ON \ - -DModule_Montage:BOOL=ON \ -DModule_FastBilateral:BOOL=ON \ -DModule_GenericLabelInterpolator:BOOL=ON \ -DModule_HigherOrderAccurateGradient:BOOL=ON \ + -DModule_IOFDF:BOOL=ON \ + -DModule_IOMeshMZ3:BOOL=ON \ -DModule_IOMeshSTL:BOOL=ON \ + -DModule_IsotropicWavelets:BOOL=ON \ -DModule_LabelErodeDilate:BOOL=ON \ -DModule_MeshNoise:BOOL=ON \ -DModule_MGHIO:BOOL=ON \ - -DModule_MinimalPathExtraction:BOOL=ON \ + -DModule_Montage:BOOL=ON \ + -DModule_MorphologicalContourInterpolation:BOOL=ON \ + -DModule_MultipleImageIterator:BOOL=ON \ + -DModule_ParabolicMorphology:BOOL=ON \ -DModule_PolarTransform:BOOL=ON \ -DModule_PrincipalComponentsAnalysis:BOOL=ON \ + -DModule_RLEImage:BOOL=ON \ -DModule_SmoothingRecursiveYvvGaussianFilter:BOOL=ON \ -DModule_SplitComponents:BOOL=ON \ - -DModule_Strain:BOOL=ON \ - -DModule_IOMeshMZ3:BOOL=ON \ - -DModule_IOFDF:BOOL=ON \ - -DModule_Thickness3D:BOOL=ON \ - -DModule_MorphologicalContourInterpolation:BOOL=ON \ - -DModule_ParabolicMorphology:BOOL=ON \ - -DModule_MultipleImageIterator:BOOL=ON \ - -DModule_RLEImage:BOOL=ON \ -DModule_SubdivisionQuadEdgeMeshFilter:BOOL=ON \ -DModule_TextureFeatures:BOOL=ON \ -DITK_COMPUTER_MEMORY_SIZE:STRING=11 \