diff --git a/packages/compare-images/typescript/package.json b/packages/compare-images/typescript/package.json index 8aceb5d5b..b3fd7951d 100644 --- a/packages/compare-images/typescript/package.json +++ b/packages/compare-images/typescript/package.json @@ -62,4 +62,4 @@ "type": "git", "url": "https://github.com/InsightSoftwareConsortium/ITK-Wasm" } -} +} \ No newline at end of file diff --git a/packages/dicom/CMakeLists.txt b/packages/dicom/CMakeLists.txt index eb45f2317..c7f9c0e4b 100644 --- a/packages/dicom/CMakeLists.txt +++ b/packages/dicom/CMakeLists.txt @@ -7,3 +7,12 @@ enable_testing() add_subdirectory(gdcm) add_subdirectory(dcmtk) + +add_test(NAME image-sets-normalization-help COMMAND image-sets-normalization --help) + +add_test(NAME image-sets-normalization-smoke + COMMAND image-sets-normalization image-sets.json --files + ${CMAKE_CURRENT_SOURCE_DIR}/test/data/input/DicomImageOrientationTest/ImageOrientation.1.dcm + ${CMAKE_CURRENT_SOURCE_DIR}/test/data/input/DicomImageOrientationTest/ImageOrientation.2.dcm + ${CMAKE_CURRENT_SOURCE_DIR}/test/data/input/DicomImageOrientationTest/ImageOrientation.3.dcm +) \ No newline at end of file diff --git a/packages/dicom/gdcm/CMakeLists.txt b/packages/dicom/gdcm/CMakeLists.txt index 99bb9320b..e7654b4e0 100644 --- a/packages/dicom/gdcm/CMakeLists.txt +++ b/packages/dicom/gdcm/CMakeLists.txt @@ -10,6 +10,9 @@ include(${ITK_USE_FILE}) add_executable(read-image-dicom-file-series read-image-dicom-file-series.cxx) target_link_libraries(read-image-dicom-file-series PUBLIC ${ITK_LIBRARIES}) +add_executable(image-sets-normalization image-sets-normalization.cxx) +target_link_libraries(image-sets-normalization PUBLIC ${ITK_LIBRARIES}) + if (WASI) return() endif() diff --git a/packages/dicom/gdcm/CharStringToUTF8Converter.h b/packages/dicom/gdcm/CharStringToUTF8Converter.h new file mode 100644 index 000000000..64ddf5f4e --- /dev/null +++ b/packages/dicom/gdcm/CharStringToUTF8Converter.h @@ -0,0 +1,467 @@ +/*========================================================================= + * + * 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 CHAR_STRING_TO_UTF8_CONVERTER_H +#define CHAR_STRING_TO_UTF8_CONVERTER_H + +#include +#include +#include + +#include + +const std::string DEFAULT_ENCODING("ISO_IR 6"); +const std::string DEFAULT_ISO_2022_ENCODING("ISO 2022 IR 6"); +constexpr const char *ASCII = "ASCII"; + +// delimiters: CR, LF, FF, ESC, TAB (see +// https://dicom.nema.org/medical/dicom/current/output/html/part05.html#sect_6.1.3, +// table 6.1-1) +// Also includes 05/12 (BACKSLASH in IR 13 or YEN SIGN in IR 14), since that +// separates Data Element Values and it resets to initial charset. +// See: dicom part 5, sect 6.1.2.5.3 +constexpr const char *DEFAULT_DELIMS = "\x1b\x09\x0a\x0c\x0d\x5c"; +// DEFAULT_DELIMS + "^" and "=" +constexpr const char *PATIENT_NAME_DELIMS = "\x1b\x09\x0a\x0c\x0d\x5c^="; + +// If not found, then pos == len +size_t +findDelim(const char *str, size_t len, size_t pos = 0, const char *delims = DEFAULT_DELIMS) +{ + while (pos < len && strchr(delims, str[pos]) == nullptr) + { + ++pos; + } + return pos; +} + +std::string +trimWhitespace(const std::string &term) +{ + auto start = term.begin(); + auto end = term.end(); + + while (start != end && std::isspace(*start)) + { + ++start; + } + + // need to --end once before checking isspace + do + { + --end; + } while (end != start && std::isspace(*end)); + + return std::string(start, end + 1); +} + +std::string +normalizeTerm(const std::string &term) +{ + return trimWhitespace(term); +} + +const char * +definedTermToIconvCharset(const std::string &defTerm) +{ + // be strict about comparing defined terms, so no fancy parsing + // that could possibly make these operations faster. + // See: + // https://dicom.nema.org/medical/dicom/current/output/chtml/part02/sect_D.6.2.html + if (defTerm == "ISO_IR 6" || defTerm == "ISO 2022 IR 6") + { + return ASCII; + } + if (defTerm == "ISO_IR 100" || defTerm == "ISO 2022 IR 100") + { + return "ISO-8859-1"; // Latin 1 + } + if (defTerm == "ISO_IR 101" || defTerm == "ISO 2022 IR 101") + { + return "ISO-8859-2"; // Latin 2 + } + if (defTerm == "ISO_IR 109" || defTerm == "ISO 2022 IR 109") + { + return "ISO-8859-3"; // Latin 3 + } + if (defTerm == "ISO_IR 110" || defTerm == "ISO 2022 IR 110") + { + return "ISO-8859-4"; // Latin 4 + } + if (defTerm == "ISO_IR 144" || defTerm == "ISO 2022 IR 144") + { + return "ISO-8859-5"; // Cyrillic + } + if (defTerm == "ISO_IR 127" || defTerm == "ISO 2022 IR 127") + { + return "ISO-8859-6"; // Arabic + } + if (defTerm == "ISO_IR 126" || defTerm == "ISO 2022 IR 126") + { + return "ISO-8859-7"; // Greek + } + if (defTerm == "ISO_IR 138" || defTerm == "ISO 2022 IR 138") + { + return "ISO-8859-8"; // Hebrew + } + if (defTerm == "ISO_IR 148" || defTerm == "ISO 2022 IR 148") + { + return "ISO-8859-9"; // Latin 5, Turkish + } + if (defTerm == "ISO_IR 13" || defTerm == "ISO 2022 IR 13") + { + // while technically not strict, SHIFT_JIS succeeds JIS X 0201 + // See: https://en.wikipedia.org/wiki/JIS_X_0201 + return "SHIFT_JIS"; // Japanese + } + if (defTerm == "ISO_IR 166" || defTerm == "ISO 2022 IR 166") + { + return "TIS-620"; // Thai + } + if (defTerm == "ISO 2022 IR 87") + { + // see: https://en.wikipedia.org/wiki/JIS_X_0208 + return "ISO-2022-JP"; // Japanese + } + if (defTerm == "ISO 2022 IR 159") + { + // see: https://en.wikipedia.org/wiki/JIS_X_0212 + return "ISO-2022-JP-1"; // Japanese + } + if (defTerm == "ISO 2022 IR 149") + { + return "EUC-KR"; // Korean + } + if (defTerm == "ISO 2022 IR 58") + { + return "EUC-CN"; // Chinese + } + if (defTerm == "ISO_IR 192") + { + return "UTF-8"; + } + if (defTerm == "GB18030") + { + return "GB18030"; + } + if (defTerm == "GBK") + { + return "GBK"; + } + return nullptr; +} + +// seq should be the sequence after the ESC char +// return value should match in definedTermToIconvCharset +const char * +iso2022EscSelectCharset(const char *seq) +{ + if (seq[0] == '(' && seq[1] == 'B') + { + return "ISO 2022 IR 6"; + } + if (seq[0] == '-' && seq[1] == 'A') + { + return "ISO 2022 IR 100"; + } + if (seq[0] == '-' && seq[1] == 'B') + { + return "ISO 2022 IR 101"; + } + if (seq[0] == '-' && seq[1] == 'C') + { + return "ISO 2022 IR 109"; + } + if (seq[0] == '-' && seq[1] == 'D') + { + return "ISO 2022 IR 110"; + } + if (seq[0] == '-' && seq[1] == 'L') + { + return "ISO 2022 IR 144"; + } + if (seq[0] == '-' && seq[1] == 'G') + { + return "ISO 2022 IR 127"; + } + if (seq[0] == '-' && seq[1] == 'F') + { + return "ISO 2022 IR 126"; + } + if (seq[0] == '-' && seq[1] == 'H') + { + return "ISO 2022 IR 138"; + } + if (seq[0] == '-' && seq[1] == 'M') + { + return "ISO 2022 IR 148"; + } + // technically 'J' corresponds to IR 14, byt SHIFT_JIS should still work + if (seq[0] == '-' && (seq[1] == 'I' || seq[1] == 'J')) + { + return "ISO 2022 IR 13"; + } + if (seq[0] == '-' && seq[1] == 'T') + { + return "ISO 2022 IR 166"; + } + if (seq[0] == '$' && seq[1] == 'B') + { + return "ISO 2022 IR 87"; + } + if (seq[0] == '$' && seq[1] == '(' && seq[2] == 'D') + { + return "ISO 2022 IR 159"; + } + if (seq[0] == '$' && seq[1] == ')' && seq[2] == 'C') + { + return "ISO 2022 IR 149"; + } + if (seq[0] == '$' && seq[1] == ')' && seq[2] == 'A') + { + return "ISO 2022 IR 58"; + } + if ((seq[0] == ')' && seq[1] == 'I') || (seq[0] == '(' && seq[1] == 'J')) + { + return "ISO 2022 IR 13"; + } + return ""; +} + +// seq should point after the ESC char. Returned length will +// not include ESC char. +size_t +iso2022EscSeqLength(const char *seq) +{ + if (seq[0] == '$' && seq[1] >= '(' && seq[1] <= '/') + { + return 3; + } + return 2; +} + +class CharStringToUTF8Converter +{ +public: + // See: setSpecificCharacterSet(const char *) + CharStringToUTF8Converter(const std::string &spcharsets) + : CharStringToUTF8Converter(spcharsets.c_str()) + { + } + CharStringToUTF8Converter(const char *spcharsets) + : handlePatientName(false) + { + this->setSpecificCharacterSet(spcharsets); + }; + + /** + * Input must be the DICOM SpecificCharacterSet element value. + * See: + * https://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.12.1.1.2 + */ + void + setSpecificCharacterSet(const char *spcharsets) + { + std::string specificCharacterSet(spcharsets); + std::string token; + std::istringstream tokStream(specificCharacterSet); + + m_charsets.clear(); + + int count = 0; + while (std::getline(tokStream, token, '\\')) + { + token = normalizeTerm(token); + + // case: first element is empty. Use default ISO-IR 6 encoding. + if (token.size() == 0 && count == 0) + { + m_charsets.push_back(DEFAULT_ENCODING); + // "Hack" to handle case where ISO-646 (dicom default encoding) is + // implicitly first in the list. Since we check for charset existence when + // switching charsets as per ISO 2022, we put both regular and ISO 2022 + // names for the default encoding. + m_charsets.push_back(DEFAULT_ISO_2022_ENCODING); + } + else if (m_charsets.end() == std::find(m_charsets.begin(), m_charsets.end(), token)) + { + // case: no duplicates + const char *chname = definedTermToIconvCharset(token); + // handle charsets that do not allow code extensions + if (count > 0 && (token == "GB18030" || token == "GBK" || token == "ISO_IR 192")) + { + std::cerr << "WARN: charset " << token << " does not support code extensions; ignoring" << std::endl; + } + else if (chname != nullptr && chname != ASCII) + { + // ISO_IR 6 isn't a formally recognized defined term, so use ASCII + // above. + m_charsets.push_back(token); + } + } + else + { + std::cerr << "WARN: Found duplicate charset '" + token + "'; ignoring" << std::endl; + } + ++count; + } + + if (count == 0) + { + // use default encoding + m_charsets.push_back(DEFAULT_ENCODING); + } + + if (m_charsets.size() == 0) + { + std::cerr << "WARN: Found no suitable charsets!" << std::endl; + } + } + + std::string + convertCharStringToUTF8(const std::string &str) const + { + size_t len = str.size(); + return this->convertCharStringToUTF8(str.c_str(), len); + } + + std::string + convertCharStringToUTF8(const char *str, size_t len) const + { + // m_charsets must always have at least 1 element prior to calling + const char *initialCharset = definedTermToIconvCharset(m_charsets[0]); + if (initialCharset == nullptr) + { + return {}; + } + + iconv_t cd = iconv_open("UTF-8", initialCharset); + if (cd == (iconv_t)-1) + { + return {}; + } + + int utf8len = len * 4; + std::unique_ptr result(new char[utf8len + 1]()); // UTF8 will have max length of utf8len + + // make a copy because iconv requires a char * + char *copiedStr = (char *)malloc(len + 1); + strncpy(copiedStr, str, len); + + char *inbuf = copiedStr; + char *outbuf = result.get(); + size_t inbytesleft = len; + size_t outbytesleft = utf8len; + + // special case: only one charset, so assume string is just that charset. + if (m_charsets.size() == 1) + { + iconv(cd, &inbuf, &inbytesleft, &outbuf, &outbytesleft); + } + else + { + size_t fragmentStart = 0; + size_t fragmentEnd = 0; + + while (fragmentStart < len) + { + const char *delims = this->handlePatientName ? PATIENT_NAME_DELIMS : DEFAULT_DELIMS; + // fragmentEnd will always be end of current fragment (exclusive end) + fragmentEnd = findDelim(str, len, fragmentStart + 1, delims); + inbuf = copiedStr + fragmentStart; + inbytesleft = fragmentEnd - fragmentStart; + + iconv(cd, &inbuf, &inbytesleft, &outbuf, &outbytesleft); + + fragmentStart = fragmentEnd; + bool isEsc = str[fragmentEnd] == 0x1b; + + if (fragmentStart < len) + { + const char *nextCharset; + int seek = 0; + + if (isEsc) + { // case: ISO 2022 escape encountered + const char *escSeq = copiedStr + fragmentStart + 1; + + const char *nextTerm = iso2022EscSelectCharset(escSeq); + nextCharset = definedTermToIconvCharset(std::string(nextTerm)); + if (nextCharset == nullptr || m_charsets.end() == std::find(m_charsets.begin(), m_charsets.end(), nextTerm)) + { + std::cerr << "WARN: bailing because invalid charset: " << nextTerm << std::endl; + break; // bail out + } + + // ISO-2022-JP is a variant on ISO 2022 for japanese, and so + // it defines its own escape sequences. As such, do not skip the + // escape sequences for ISO-2022-JP, so iconv can properly interpret + // them. + if (0 != strcmp("ISO-2022-JP", nextCharset) && 0 != strcmp("ISO-2022-JP-1", nextCharset)) + { + seek = iso2022EscSeqLength(escSeq) + 1; + } + } + else + { // case: hit a CR, LF, or FF + // reset to initial charset + nextCharset = initialCharset; + } + + if (0 != iconv_close(cd)) + { + std::cerr << "WARN: bailing because iconv_close" << std::endl; + break; // bail out + } + cd = iconv_open("UTF-8", nextCharset); + if (cd == (iconv_t)-1) + { + std::cerr << "WARN: bailing because iconv_open" << std::endl; + break; // bail out + } + + fragmentStart += seek; + } + } + } + + free(copiedStr); + iconv_close(cd); + + // since result is filled with NULL bytes, string constructor will figure out + // the correct string ending. + return std::string(result.get()); + } + + bool + getHandlePatientName() + { + return this->handlePatientName; + } + + void + setHandlePatientName(bool yn) + { + this->handlePatientName = yn; + } + +private: + std::vector m_charsets; + bool handlePatientName; +}; + +#endif // CHAR_STRING_TO_UTF8_CONVERTER_H \ No newline at end of file diff --git a/packages/dicom/gdcm/DICOMTagReader.h b/packages/dicom/gdcm/DICOMTagReader.h new file mode 100644 index 000000000..59aed7622 --- /dev/null +++ b/packages/dicom/gdcm/DICOMTagReader.h @@ -0,0 +1,144 @@ +/*========================================================================= + * + * 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 DICOM_TAG_READER_H +#define DICOM_TAG_READER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "rapidjson/document.h" +#include "rapidjson/stringbuffer.h" +#include "rapidjson/writer.h" + +#include "itkCommonEnums.h" +#include "itkGDCMImageIO.h" +#include "itkGDCMSeriesFileNames.h" +#include "itkImageIOBase.h" +#include "itkMetaDataObject.h" + +#include "CharStringToUTF8Converter.h" + +std::string +unpackMetaAsString(const itk::MetaDataObjectBase::Pointer &metaValue) +{ + using MetaDataStringType = itk::MetaDataObject; + MetaDataStringType::Pointer value = dynamic_cast(metaValue.GetPointer()); + if (value != nullptr) + { + return value->GetMetaDataObjectValue(); + } + return {}; +} + +namespace itk +{ + + /** \class DICOMTagReader + * + * \brief Reads DICOM tags from a DICOM object. + */ + class DICOMTagReader + { + public: + using MetaDictType = itk::MetaDataDictionary; + using TagMapType = std::unordered_map; + + DICOMTagReader() + : m_dirtyCache(true) + { + m_GDCMImageIO = GDCMImageIO::New(); + } + + /** Sets file name. */ + void + SetFileName(const std::string &file) + { + m_fileName = file; + m_GDCMImageIO->SetFileName(file); + m_dirtyCache = true; + } + + /** Verify file can be read. */ + bool + CanReadFile(const std::string &file) + { + return m_GDCMImageIO->CanReadFile(file.c_str()); + } + + std::string + ReadTag(const std::string &tag) + { + + if (m_dirtyCache) + { + m_GDCMImageIO->SetUseStreamedReading(true); + m_GDCMImageIO->ReadImageInformation(); + m_tagDict = m_GDCMImageIO->GetMetaDataDictionary(); + auto specificCharacterSet = unpackMetaAsString(m_tagDict["0008|0005"]); + m_decoder = CharStringToUTF8Converter(specificCharacterSet); + m_dirtyCache = false; + } + + auto value = unpackMetaAsString(m_tagDict[tag]); + return m_decoder.convertCharStringToUTF8(value); + } + + TagMapType + ReadAllTags() + { + + if (m_dirtyCache) + { + m_GDCMImageIO->SetUseStreamedReading(true); + m_GDCMImageIO->ReadImageInformation(); + m_tagDict = m_GDCMImageIO->GetMetaDataDictionary(); + auto specificCharacterSet = unpackMetaAsString(m_tagDict["0008|0005"]); + m_decoder = CharStringToUTF8Converter(specificCharacterSet); + m_dirtyCache = false; + } + + TagMapType allTagsDict; + for (auto it = m_tagDict.Begin(); it != m_tagDict.End(); ++it) + { + auto value = unpackMetaAsString(it->second); + allTagsDict[it->first] = m_decoder.convertCharStringToUTF8(value); + } + + return allTagsDict; + } + + private: + std::string m_fileName; + itk::GDCMImageIO::Pointer m_GDCMImageIO; + MetaDictType m_tagDict; + CharStringToUTF8Converter m_decoder = CharStringToUTF8Converter(""); + bool m_dirtyCache; + }; + +} // end namespace itk + +#endif // DICOM_TAG_READER_H \ No newline at end of file diff --git a/packages/dicom/gdcm/SortSpatially.h b/packages/dicom/gdcm/SortSpatially.h new file mode 100644 index 000000000..832b47bd4 --- /dev/null +++ b/packages/dicom/gdcm/SortSpatially.h @@ -0,0 +1,94 @@ +/*========================================================================= + * + * 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 SORT_SPATIALLY_H +#define SORT_SPATIALLY_H + +#include +#include + +#include "gdcmSerieHelper.h" + +class CustomSerieHelper : public gdcm::SerieHelper +{ +public: + void AddFileName(std::string const &fileName) + { + SerieHelper::AddFileName(fileName); + } +}; + +using FileNamesContainer = std::vector; + +FileNamesContainer sortSpatially(std::vector unsortedSerieFileNames) +{ + std::unique_ptr serieHelper(new CustomSerieHelper()); + for (const std::string &fileName : unsortedSerieFileNames) + { + serieHelper->AddFileName(fileName); + } + serieHelper->SetUseSeriesDetails(true); + // Add the default restrictions to refine the file set into multiple series. + serieHelper->CreateDefaultUniqueSeriesIdentifier(); + using SeriesIdContainer = std::vector; + SeriesIdContainer seriesUIDs; + // Accessing the first serie found (assume there is at least one) + gdcm::FileList *flist = serieHelper->GetFirstSingleSerieUIDFileSet(); + while (flist) + { + if (!flist->empty()) // make sure we have at least one serie + { + gdcm::File *file = (*flist)[0]; // for example take the first one + + // Create its unique series ID + const std::string id(serieHelper->CreateUniqueSeriesIdentifier(file)); + + seriesUIDs.push_back(id); + } + flist = serieHelper->GetNextSingleSerieUIDFileSet(); + } + + FileNamesContainer fileNames; + flist = serieHelper->GetFirstSingleSerieUIDFileSet(); + const std::string serie = seriesUIDs[0]; + bool found = false; + while (flist && !found) + { + if (!flist->empty()) // make sure we have at least one serie + { + gdcm::File *file = (*flist)[0]; // for example take the first one + const std::string id(serieHelper->CreateUniqueSeriesIdentifier(file)); + if (id == serie) + { + found = true; // we found a match + break; + } + } + flist = serieHelper->GetNextSingleSerieUIDFileSet(); + } + serieHelper->OrderFileList(flist); + + gdcm::FileList::iterator it; + for (it = flist->begin(); it != flist->end(); ++it) + { + gdcm::FileWithName *header = *it; + fileNames.push_back(header->filename); + } + return fileNames; +} + +#endif // SORT_SPATIALLY_H \ No newline at end of file diff --git a/packages/dicom/gdcm/Tags.h b/packages/dicom/gdcm/Tags.h new file mode 100644 index 000000000..9a9014b46 --- /dev/null +++ b/packages/dicom/gdcm/Tags.h @@ -0,0 +1,369 @@ +/*========================================================================= + * + * 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 TAGS_H +#define TAGS_H + +#include +#include + +using Tag = gdcm::Tag; +using Tags = std::set; + +const Tag STUDY_UID(0x0020, 0x000d); // "Study Instance UID" +const Tag SERIES_UID(0x0020, 0x000e); // "Series Instance UID" +const Tag INSTANCE_UID(0x0008, 0x0018); // "Instance UID" + +const Tag FRAME_OF_REFERENCE_UID(0x0020, 0x0052); +const Tag IMAGE_ORIENTATION_PATIENT(0x0020, 0x0037); + +const Tag SPECIFIC_CHARACTER_SET(0x0008, 0x0005); +const Tag PIXEL_DATA_TAG(0x7fe0, 0x0010); + +const Tags EMPTY_TAGS = {}; + +// Tag names from https://docs.aws.amazon.com/healthimaging/latest/devguide/reference-dicom-support.html +const Tags PATIENT_TAGS = { + // Patient Module Elements + Tag(0x0010, 0x0010), // "Patient's Name" + Tag(0x0010, 0x0020), // "Patient ID" + // Issuer of Patient ID Macro Elements + Tag(0x0010, 0x0021), // "Issuer of Patient ID" + Tag(0x0010, 0x0024), // "Issuer of Patient ID Qualifiers Sequence" + Tag(0x0010, 0x0022), // "Type of Patient ID" + Tag(0x0010, 0x0030), // "Patient's Birth Date" + Tag(0x0010, 0x0033), // "Patient's Birth Date in Alternative Calendar" + Tag(0x0010, 0x0034), // "Patient's Death Date in Alternative Calendar" + Tag(0x0010, 0x0035), // "Patient's Alternative Calendar Attribute" + Tag(0x0010, 0x0040), // "Patient's Sex" + Tag(0x0010, 0x1100), // "Referenced Patient Photo Sequence" + Tag(0x0010, 0x0200), // "Quality Control Subject" + Tag(0x0008, 0x1120), // "Referenced Patient Sequence" + Tag(0x0010, 0x0032), // "Patient's Birth Time" + Tag(0x0010, 0x1002), // "Other Patient IDs Sequence" + Tag(0x0010, 0x1001), // "Other Patient Names" + Tag(0x0010, 0x2160), // "Ethnic Group" + Tag(0x0010, 0x4000), // "Patient Comments" + Tag(0x0010, 0x2201), // "Patient Species Description" + Tag(0x0010, 0x2202), // "Patient Species Code Sequence Attribute" + Tag(0x0010, 0x2292), // "Patient Breed Description" + Tag(0x0010, 0x2293), // "Patient Breed Code Sequence" + Tag(0x0010, 0x2294), // "Breed Registration Sequence Attribute" + Tag(0x0010, 0x0212), // "Strain Description" + Tag(0x0010, 0x0213), // "Strain Nomenclature Attribute" + Tag(0x0010, 0x0219), // "Strain Code Sequence" + Tag(0x0010, 0x0218), // "Strain Additional Information Attribute" + Tag(0x0010, 0x0216), // "Strain Stock Sequence" + Tag(0x0010, 0x0221), // "Genetic Modifications Sequence Attribute" + Tag(0x0010, 0x2297), // "Responsible Person" + Tag(0x0010, 0x2298), // "Responsible Person Role Attribute" + Tag(0x0010, 0x2299), // "Responsible Organization" + Tag(0x0012, 0x0062), // "Patient Identity Removed" + Tag(0x0012, 0x0063), // "De-identification Method" + Tag(0x0012, 0x0064), // "De-identification Method Code Sequence" + // Patient Group Macro Elements + Tag(0x0010, 0x0026), // "Source Patient Group Identification Sequence" + Tag(0x0010, 0x0027), // "Group of Patients Identification Sequence" + // Clinical Trial Subject Module + Tag(0x0012, 0x0010), // "Clinical Trial Sponsor Name" + Tag(0x0012, 0x0020), // "Clinical Trial Protocol ID" + Tag(0x0012, 0x0021), // "Clinical Trial Protocol Name Attribute" + Tag(0x0012, 0x0030), // "Clinical Trial Site ID" + Tag(0x0012, 0x0031), // "Clinical Trial Site Name" + Tag(0x0012, 0x0040), // "Clinical Trial Subject ID" + Tag(0x0012, 0x0042), // "Clinical Trial Subject Reading ID" + Tag(0x0012, 0x0081), // "Clinical Trial Protocol Ethics Committee Name" + Tag(0x0012, 0x0082) // "Clinical Trial Protocol Ethics Committee Approval Number" +}; + +const Tags STUDY_TAGS = { + // General Study Module + Tag(0x0020, 0x000d), // "Study Instance UID" + Tag(0x0008, 0x0020), // "Study Date" + Tag(0x0008, 0x0030), // "Study Time" + Tag(0x0008, 0x0090), // "Referring Physician's Name" + Tag(0x0008, 0x0096), // "Referring Physician Identification Sequence" + Tag(0x0008, 0x009c), // "Consulting Physician's Name" + Tag(0x0008, 0x009d), // "Consulting Physician Identification Sequence" + Tag(0x0020, 0x0010), // "Study ID" + Tag(0x0008, 0x0050), // "Accession Number" + Tag(0x0008, 0x0051), // "Issuer of Accession Number Sequence" + Tag(0x0008, 0x1030), // "Study Description" + Tag(0x0008, 0x1048), // "Physician(s) of Record" + Tag(0x0008, 0x1049), // "Physician(s) of Record Identification Sequence" + Tag(0x0008, 0x1060), // "Name of Physician(s) Reading Study" + Tag(0x0008, 0x1062), // "Physician(s) Reading Study Identification Sequence" + Tag(0x0032, 0x1033), // "Requesting Service" + Tag(0x0032, 0x1034), // "Requesting Service Code Sequence" + Tag(0x0008, 0x1110), // "Referenced Study Sequence" + Tag(0x0008, 0x1032), // "Procedure Code Sequence" + Tag(0x0040, 0x1012), // "Reason For Performed Procedure Code Sequence" + // Patient Study Module + Tag(0x0008, 0x1080), // "Admitting Diagnoses Description" + Tag(0x0008, 0x1084), // "Admitting Diagnoses Code Sequence" + Tag(0x0010, 0x1010), // "Patient's Age" + Tag(0x0010, 0x1020), // "Patient's Size" + Tag(0x0010, 0x1030), // "Patient's Weight" + Tag(0x0010, 0x1022), // "Patient's Body Mass Index" + Tag(0x0010, 0x1023), // "Measured AP Dimension" + Tag(0x0010, 0x1024), // "Measured Lateral Dimension" + Tag(0x0010, 0x1021), // "Patient's Size Code Sequence" + Tag(0x0010, 0x2000), // "Medical Alerts" + Tag(0x0010, 0x2110), // "Allergies" + Tag(0x0010, 0x21a0), // "Smoking Status" + Tag(0x0010, 0x21c0), // "Pregnancy Status" + Tag(0x0010, 0x21d0), // "Last Menstrual Date" + Tag(0x0038, 0x0500), // "Patient State" + Tag(0x0010, 0x2180), // "Occupation" + Tag(0x0010, 0x21b0), // "Additional Patient History" + Tag(0x0038, 0x0010), // "Admission ID" + Tag(0x0038, 0x0014), // "Issuer of Admission ID Sequence" + Tag(0x0032, 0x1066), // "Reason for Visit" + Tag(0x0032, 0x1067), // "Reason for Visit Code Sequence" + Tag(0x0038, 0x0060), // "Service Episode ID" + Tag(0x0038, 0x0064), // "Issuer of Service Episode ID Sequence" + Tag(0x0038, 0x0062), // "Service Episode Description" + Tag(0x0010, 0x2203), // "Patient's Sex Neutered" + // Clinical Trial Study Module + Tag(0x0012, 0x0050), // "Clinical Trial Time Point ID" + Tag(0x0012, 0x0051), // "Clinical Trial Time Point Description" + Tag(0x0012, 0x0052), // "Longitudinal Temporal Offset from Event" + Tag(0x0012, 0x0053), // "Longitudinal Temporal Event Type" + Tag(0x0012, 0x0083) // "Consent for Clinical Trial Use Sequence" +}; + +const Tags SERIES_TAGS = { + // General Series Module + Tag(0x0008, 0x0060), // "Modality" + Tag(0x0020, 0x000e), // "Series Instance UID" + Tag(0x0020, 0x0011), // "Series Number" + Tag(0x0020, 0x0060), // "Laterality" + Tag(0x0008, 0x0021), // "Series Date" + Tag(0x0008, 0x0031), // "Series Time" + Tag(0x0008, 0x1050), // "Performing Physician's Name" + Tag(0x0008, 0x1052), // "Performing Physician Identification Sequence" + Tag(0x0018, 0x1030), // "Protocol Name" + Tag(0x0008, 0x103e), // "Series Description" + Tag(0x0008, 0x103f), // "Series Description Code Sequence" + Tag(0x0008, 0x1070), // "Operators' Name" + Tag(0x0008, 0x1072), // "Operator Identification Sequence" + Tag(0x0008, 0x1111), // "Referenced Performed Procedure Step Sequence" + Tag(0x0008, 0x1250), // "Related Series Sequence" + Tag(0x0018, 0x0015), // "Body Part Examined" + Tag(0x0018, 0x5100), // "Patient Position" + Tag(0x0028, 0x0108), // "Smallest Pixel Value in Series" + Tag(0x0028, 0x0109), // "Largest Pixel Value in Series" + Tag(0x0040, 0x0275), // "Request Attributes Sequence" + Tag(0x0010, 0x2210), // "Anatomical Orientation Type" + Tag(0x300a, 0x0700), // "Treatment Session UID" + // Clinical Trial Series Module + Tag(0x0012, 0x0060), // "Clinical Trial Coordinating Center Name" + Tag(0x0012, 0x0071), // "Clinical Trial Series ID" + Tag(0x0012, 0x0072), // "Clinical Trial Series Description" + // General Equipment Module + Tag(0x0008, 0x0070), // "Manufacturer" + Tag(0x0008, 0x0080), // "Institution Name" + Tag(0x0008, 0x0081), // "Institution Address" + Tag(0x0008, 0x1010), // "Station Name" + Tag(0x0008, 0x1040), // "Institutional Department Name" + Tag(0x0008, 0x1041), // "Institutional Department Type Code Sequence" + Tag(0x0008, 0x1090), // "Manufacturer's Model Name" + Tag(0x0018, 0x100b), // "Manufacturer's Device Class UID" + Tag(0x0018, 0x1000), // "Device Serial Number" + Tag(0x0018, 0x1020), // "Software Versions" + Tag(0x0018, 0x1008), // "Gantry ID" + Tag(0x0018, 0x100a), // "UDI Sequence" + Tag(0x0018, 0x1002), // "Device UID" + Tag(0x0018, 0x1050), // "Spatial Resolution" + Tag(0x0018, 0x1200), // "Date of Last Calibration" + Tag(0x0018, 0x1201), // "Time of Last Calibration" + Tag(0x0028, 0x0120), // "Pixel Padding Value" + // Frame of Reference Module + Tag(0x0020, 0x0052), // "Frame of Reference UID" + Tag(0x0020, 0x1040), // "Position Reference Indicator" +}; + +const Tags NON_INSTANCE = { + // Patient Module Elements + Tag(0x0010, 0x0010), // "Patient's Name" + Tag(0x0010, 0x0020), // "Patient ID" + // Issuer of Patient ID Macro Elements + Tag(0x0010, 0x0021), // "Issuer of Patient ID" + Tag(0x0010, 0x0024), // "Issuer of Patient ID Qualifiers Sequence" + Tag(0x0010, 0x0022), // "Type of Patient ID" + Tag(0x0010, 0x0030), // "Patient's Birth Date" + Tag(0x0010, 0x0033), // "Patient's Birth Date in Alternative Calendar" + Tag(0x0010, 0x0034), // "Patient's Death Date in Alternative Calendar" + Tag(0x0010, 0x0035), // "Patient's Alternative Calendar Attribute" + Tag(0x0010, 0x0040), // "Patient's Sex" + Tag(0x0010, 0x1100), // "Referenced Patient Photo Sequence" + Tag(0x0010, 0x0200), // "Quality Control Subject" + Tag(0x0008, 0x1120), // "Referenced Patient Sequence" + Tag(0x0010, 0x0032), // "Patient's Birth Time" + Tag(0x0010, 0x1002), // "Other Patient IDs Sequence" + Tag(0x0010, 0x1001), // "Other Patient Names" + Tag(0x0010, 0x2160), // "Ethnic Group" + Tag(0x0010, 0x4000), // "Patient Comments" + Tag(0x0010, 0x2201), // "Patient Species Description" + Tag(0x0010, 0x2202), // "Patient Species Code Sequence Attribute" + Tag(0x0010, 0x2292), // "Patient Breed Description" + Tag(0x0010, 0x2293), // "Patient Breed Code Sequence" + Tag(0x0010, 0x2294), // "Breed Registration Sequence Attribute" + Tag(0x0010, 0x0212), // "Strain Description" + Tag(0x0010, 0x0213), // "Strain Nomenclature Attribute" + Tag(0x0010, 0x0219), // "Strain Code Sequence" + Tag(0x0010, 0x0218), // "Strain Additional Information Attribute" + Tag(0x0010, 0x0216), // "Strain Stock Sequence" + Tag(0x0010, 0x0221), // "Genetic Modifications Sequence Attribute" + Tag(0x0010, 0x2297), // "Responsible Person" + Tag(0x0010, 0x2298), // "Responsible Person Role Attribute" + Tag(0x0010, 0x2299), // "Responsible Organization" + Tag(0x0012, 0x0062), // "Patient Identity Removed" + Tag(0x0012, 0x0063), // "De-identification Method" + Tag(0x0012, 0x0064), // "De-identification Method Code Sequence" + // Patient Group Macro Elements + Tag(0x0010, 0x0026), // "Source Patient Group Identification Sequence" + Tag(0x0010, 0x0027), // "Group of Patients Identification Sequence" + // Clinical Trial Subject Module + Tag(0x0012, 0x0010), // "Clinical Trial Sponsor Name" + Tag(0x0012, 0x0020), // "Clinical Trial Protocol ID" + Tag(0x0012, 0x0021), // "Clinical Trial Protocol Name Attribute" + Tag(0x0012, 0x0030), // "Clinical Trial Site ID" + Tag(0x0012, 0x0031), // "Clinical Trial Site Name" + Tag(0x0012, 0x0040), // "Clinical Trial Subject ID" + Tag(0x0012, 0x0042), // "Clinical Trial Subject Reading ID" + Tag(0x0012, 0x0081), // "Clinical Trial Protocol Ethics Committee Name" + Tag(0x0012, 0x0082), // "Clinical Trial Protocol Ethics Committee Approval Number" + // General Study Module + Tag(0x0020, 0x000d), // "Study Instance UID" + Tag(0x0008, 0x0020), // "Study Date" + Tag(0x0008, 0x0030), // "Study Time" + Tag(0x0008, 0x0090), // "Referring Physician's Name" + Tag(0x0008, 0x0096), // "Referring Physician Identification Sequence" + Tag(0x0008, 0x009c), // "Consulting Physician's Name" + Tag(0x0008, 0x009d), // "Consulting Physician Identification Sequence" + Tag(0x0020, 0x0010), // "Study ID" + Tag(0x0008, 0x0050), // "Accession Number" + Tag(0x0008, 0x0051), // "Issuer of Accession Number Sequence" + Tag(0x0008, 0x1030), // "Study Description" + Tag(0x0008, 0x1048), // "Physician(s) of Record" + Tag(0x0008, 0x1049), // "Physician(s) of Record Identification Sequence" + Tag(0x0008, 0x1060), // "Name of Physician(s) Reading Study" + Tag(0x0008, 0x1062), // "Physician(s) Reading Study Identification Sequence" + Tag(0x0032, 0x1033), // "Requesting Service" + Tag(0x0032, 0x1034), // "Requesting Service Code Sequence" + Tag(0x0008, 0x1110), // "Referenced Study Sequence" + Tag(0x0008, 0x1032), // "Procedure Code Sequence" + Tag(0x0040, 0x1012), // "Reason For Performed Procedure Code Sequence" + // Patient Study Module + Tag(0x0008, 0x1080), // "Admitting Diagnoses Description" + Tag(0x0008, 0x1084), // "Admitting Diagnoses Code Sequence" + Tag(0x0010, 0x1010), // "Patient's Age" + Tag(0x0010, 0x1020), // "Patient's Size" + Tag(0x0010, 0x1030), // "Patient's Weight" + Tag(0x0010, 0x1022), // "Patient's Body Mass Index" + Tag(0x0010, 0x1023), // "Measured AP Dimension" + Tag(0x0010, 0x1024), // "Measured Lateral Dimension" + Tag(0x0010, 0x1021), // "Patient's Size Code Sequence" + Tag(0x0010, 0x2000), // "Medical Alerts" + Tag(0x0010, 0x2110), // "Allergies" + Tag(0x0010, 0x21a0), // "Smoking Status" + Tag(0x0010, 0x21c0), // "Pregnancy Status" + Tag(0x0010, 0x21d0), // "Last Menstrual Date" + Tag(0x0038, 0x0500), // "Patient State" + Tag(0x0010, 0x2180), // "Occupation" + Tag(0x0010, 0x21b0), // "Additional Patient History" + Tag(0x0038, 0x0010), // "Admission ID" + Tag(0x0038, 0x0014), // "Issuer of Admission ID Sequence" + Tag(0x0032, 0x1066), // "Reason for Visit" + Tag(0x0032, 0x1067), // "Reason for Visit Code Sequence" + Tag(0x0038, 0x0060), // "Service Episode ID" + Tag(0x0038, 0x0064), // "Issuer of Service Episode ID Sequence" + Tag(0x0038, 0x0062), // "Service Episode Description" + Tag(0x0010, 0x2203), // "Patient's Sex Neutered" + // Clinical Trial Study Module + Tag(0x0012, 0x0050), // "Clinical Trial Time Point ID" + Tag(0x0012, 0x0051), // "Clinical Trial Time Point Description" + Tag(0x0012, 0x0052), // "Longitudinal Temporal Offset from Event" + Tag(0x0012, 0x0053), // "Longitudinal Temporal Event Type" + Tag(0x0012, 0x0083), // "Consent for Clinical Trial Use Sequence" + + // General Series Module + Tag(0x0008, 0x0060), // "Modality" + Tag(0x0020, 0x000e), // "Series Instance UID" + Tag(0x0020, 0x0011), // "Series Number" + Tag(0x0020, 0x0060), // "Laterality" + Tag(0x0008, 0x0021), // "Series Date" + Tag(0x0008, 0x0031), // "Series Time" + Tag(0x0008, 0x1050), // "Performing Physician's Name" + Tag(0x0008, 0x1052), // "Performing Physician Identification Sequence" + Tag(0x0018, 0x1030), // "Protocol Name" + Tag(0x0008, 0x103e), // "Series Description" + Tag(0x0008, 0x103f), // "Series Description Code Sequence" + Tag(0x0008, 0x1070), // "Operators' Name" + Tag(0x0008, 0x1072), // "Operator Identification Sequence" + Tag(0x0008, 0x1111), // "Referenced Performed Procedure Step Sequence" + Tag(0x0008, 0x1250), // "Related Series Sequence" + Tag(0x0018, 0x0015), // "Body Part Examined" + Tag(0x0018, 0x5100), // "Patient Position" + Tag(0x0028, 0x0108), // "Smallest Pixel Value in Series" + Tag(0x0028, 0x0109), // "Largest Pixel Value in Series" + Tag(0x0040, 0x0275), // "Request Attributes Sequence" + Tag(0x0010, 0x2210), // "Anatomical Orientation Type" + Tag(0x300a, 0x0700), // "Treatment Session UID" + // Clinical Trial Series Module + Tag(0x0012, 0x0060), // "Clinical Trial Coordinating Center Name" + Tag(0x0012, 0x0071), // "Clinical Trial Series ID" + Tag(0x0012, 0x0072), // "Clinical Trial Series Description" + // General Equipment Module + Tag(0x0008, 0x0070), // "Manufacturer" + Tag(0x0008, 0x0080), // "Institution Name" + Tag(0x0008, 0x0081), // "Institution Address" + Tag(0x0008, 0x1010), // "Station Name" + Tag(0x0008, 0x1040), // "Institutional Department Name" + Tag(0x0008, 0x1041), // "Institutional Department Type Code Sequence" + Tag(0x0008, 0x1090), // "Manufacturer's Model Name" + Tag(0x0018, 0x100b), // "Manufacturer's Device Class UID" + Tag(0x0018, 0x1000), // "Device Serial Number" + Tag(0x0018, 0x1020), // "Software Versions" + Tag(0x0018, 0x1008), // "Gantry ID" + Tag(0x0018, 0x100a), // "UDI Sequence" + Tag(0x0018, 0x1002), // "Device UID" + Tag(0x0018, 0x1050), // "Spatial Resolution" + Tag(0x0018, 0x1200), // "Date of Last Calibration" + Tag(0x0018, 0x1201), // "Time of Last Calibration" + Tag(0x0028, 0x0120), // "Pixel Padding Value" + // Frame of Reference Module + Tag(0x0020, 0x0052), // "Frame of Reference UID" + Tag(0x0020, 0x1040), // "Position Reference Indicator" +}; + +std::pair +getTagBuffer(const gdcm::DataSet &ds, const gdcm::Tag &tag) +{ + if (!ds.FindDataElement(tag) || ds.GetDataElement(tag).IsEmpty()) + { + return std::make_pair(nullptr, 0); + } + const gdcm::DataElement de = ds.GetDataElement(tag); + const gdcm::ByteValue *bv = de.GetByteValue(); + const char *tagValue = bv->GetPointer(); + size_t len = bv->GetLength(); + return std::make_pair(tagValue, len); +} + +#endif // TAGS_H \ No newline at end of file diff --git a/packages/dicom/gdcm/TagsOptionParser.h b/packages/dicom/gdcm/TagsOptionParser.h new file mode 100644 index 000000000..89dc852db --- /dev/null +++ b/packages/dicom/gdcm/TagsOptionParser.h @@ -0,0 +1,62 @@ +/*========================================================================= + * + * 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 TAGS_OPTION_PARSER_H +#define TAGS_OPTION_PARSER_H + +#include +#include "rapidjson/document.h" + +#include "Tags.h" + +std::optional parseTags(itk::wasm::InputTextStream &tagsToRead, itk::wasm::Pipeline &pipeline) +{ + if (tagsToRead.GetPointer() == nullptr) + { + return std::nullopt; + } + + rapidjson::Document inputTagsDocument; + const std::string inputTagsString((std::istreambuf_iterator(tagsToRead.Get())), + std::istreambuf_iterator()); + if (inputTagsDocument.Parse(inputTagsString.c_str()).HasParseError()) + { + CLI::Error err("Runtime error", "Could not parse input tags JSON.", 1); + pipeline.exit(err); + return std::nullopt; + } + if (!inputTagsDocument.HasMember("tags")) + { + CLI::Error err("Runtime error", "Input tags does not have expected \"tags\" member", 1); + pipeline.exit(err); + return std::nullopt; + } + + const rapidjson::Value &inputTagsArray = inputTagsDocument["tags"]; + + Tags tags; + for (rapidjson::Value::ConstValueIterator itr = inputTagsArray.Begin(); itr != inputTagsArray.End(); ++itr) + { + const std::string tagString(itr->GetString()); + Tag tag; + tag.ReadFromPipeSeparatedString(tagString.c_str()); + tags.insert(tag); + } + return tags; +} + +#endif // TAGS_OPTION_PARSER_H \ No newline at end of file diff --git a/packages/dicom/gdcm/gdcmDiscriminateVolume.h b/packages/dicom/gdcm/gdcmDiscriminateVolume.h new file mode 100644 index 000000000..7b315db2a --- /dev/null +++ b/packages/dicom/gdcm/gdcmDiscriminateVolume.h @@ -0,0 +1,251 @@ +/*========================================================================= + + Program: GDCM (Grassroots DICOM). A DICOM library + + Copyright (c) 2006-2011 Mathieu Malaterre + All rights reserved. + See Copyright.txt or http://gdcm.sourceforge.net/Copyright.html 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. + +=========================================================================*/ + +/*========================================================================= + + * 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 DISCRIMINATE_VOLUME_H +#define DISCRIMINATE_VOLUME_H + +#include "gdcmScanner.h" +#include "gdcmTesting.h" +#include "gdcmIPPSorter.h" +#include "gdcmDirectionCosines.h" +#include + +/* + * The following example is a basic sorted which should work in generic cases. + * It sort files based on: + * Study Instance UID + * Series Instance UID + * Frame of Reference UID + * Image Orientation (Patient) + * Image Position (Patient) (Sorting based on IPP + IOP) + */ + +namespace gdcm +{ + const Tag t1(0x0020, 0x000d); // Study Instance UID + const Tag t2(0x0020, 0x000e); // Series Instance UID + const Tag t3(0x0020, 0x0052); // Frame of Reference UID + const Tag t4(0x0020, 0x0037); // Image Orientation (Patient) + + class DiscriminateVolume + { + private: + std::vector SortedFiles; + std::vector UnsortedFiles; + + Directory::FilenamesType GetAllFilenamesFromTagToValue( + Scanner const &s, Directory::FilenamesType const &filesubset, Tag const &t, const char *valueref) + { + Directory::FilenamesType theReturn; + if (valueref) + { + size_t len = strlen(valueref); + Directory::FilenamesType::const_iterator file = filesubset.begin(); + for (; file != filesubset.end(); ++file) + { + const char *filename = file->c_str(); + const char *value = s.GetValue(filename, t); + if (value && strncmp(value, valueref, len) == 0) + { + theReturn.push_back(filename); + } + } + } + return theReturn; + } + + void ProcessAIOP(Scanner const &, Directory::FilenamesType const &subset, const char *iopval) + { + IPPSorter ipp; + ipp.SetComputeZSpacing(true); + ipp.SetZSpacingTolerance(1e-3); // ?? + bool b = ipp.Sort(subset); + if (!b) + { + // If you reach here this means you need one more parameter to discriminiat this + // series. Eg. T1 / T2 intertwinted. Multiple Echo (0018,0081) + std::cerr << "Failed to sort: " << subset.begin()->c_str() << std::endl; + for ( + Directory::FilenamesType::const_iterator file = subset.begin(); + file != subset.end(); ++file) + { + std::cerr << *file << std::endl; + } + UnsortedFiles.push_back(subset); + return; + } + SortedFiles.push_back(ipp.GetFilenames()); + } + + void ProcessAFrameOfRef(Scanner const &s, Directory::FilenamesType const &subset, const char *frameuid) + { + // In this subset of files (belonging to same series), let's find those + // belonging to the same Frame ref UID: + Directory::FilenamesType files = GetAllFilenamesFromTagToValue( + s, subset, t3, frameuid); + + std::set iopset; + + for ( + Directory::FilenamesType::const_iterator file = files.begin(); + file != files.end(); ++file) + { + const char *value = s.GetValue(file->c_str(), gdcm::t4); + assert(value); + iopset.insert(value); + } + size_t n = iopset.size(); + if (n == 0) + { + assert(files.empty()); + return; + } + + if (n == 1) + { + ProcessAIOP(s, files, iopset.begin()->c_str()); + } + else + { + const char *f = files.begin()->c_str(); + std::cerr << "More than one IOP: " << f << std::endl; + // Make sure that there is actually 'n' different IOP + gdcm::DirectionCosines ref; + gdcm::DirectionCosines dc; + for ( + std::set::const_iterator it = iopset.begin(); + it != iopset.end(); ++it) + { + ref.SetFromString(it->c_str()); + for ( + Directory::FilenamesType::const_iterator file = files.begin(); + file != files.end(); ++file) + { + std::string value = s.GetValue(file->c_str(), gdcm::t4); + if (value != it->c_str()) + { + dc.SetFromString(value.c_str()); + const double crossdot = ref.CrossDot(dc); + const double eps = std::fabs(1. - crossdot); + if (eps < 1e-6) + { + std::cerr << "Problem with IOP discrimination: " << file->c_str() + << " " << it->c_str() << std::endl; + return; + } + } + } + } + // If we reach here this means there is actually 'n' different IOP + for ( + std::set::const_iterator it = iopset.begin(); + it != iopset.end(); ++it) + { + const char *iopvalue = it->c_str(); + Directory::FilenamesType iopfiles = GetAllFilenamesFromTagToValue( + s, files, t4, iopvalue); + ProcessAIOP(s, iopfiles, iopvalue); + } + } + } + + void ProcessASeries(Scanner const &s, const char *seriesuid) + { + // let's find all files belonging to this series: + Directory::FilenamesType seriesfiles = GetAllFilenamesFromTagToValue( + s, s.GetFilenames(), t2, seriesuid); + + gdcm::Scanner::ValuesType vt3 = s.GetValues(t3); + for ( + gdcm::Scanner::ValuesType::const_iterator it = vt3.begin(); it != vt3.end(); ++it) + { + ProcessAFrameOfRef(s, seriesfiles, it->c_str()); + } + } + + void ProcessAStudy(Scanner const &s, const char *studyuid) + { + gdcm::Scanner::ValuesType vt2 = s.GetValues(t2); + for ( + gdcm::Scanner::ValuesType::const_iterator it = vt2.begin(); it != vt2.end(); ++it) + { + ProcessASeries(s, it->c_str()); + } + } + + public: + void Print(std::ostream &os) + { + os << "Sorted Files: " << std::endl; + for ( + std::vector::const_iterator it = SortedFiles.begin(); + it != SortedFiles.end(); ++it) + { + os << "Group: " << std::endl; + for ( + Directory::FilenamesType::const_iterator file = it->begin(); + file != it->end(); ++file) + { + os << *file << std::endl; + } + } + os << "Unsorted Files: " << std::endl; + for ( + std::vector::const_iterator it = UnsortedFiles.begin(); + it != UnsortedFiles.end(); ++it) + { + os << "Group: " << std::endl; + for ( + Directory::FilenamesType::const_iterator file = it->begin(); + file != it->end(); ++file) + { + os << *file << std::endl; + } + } + } + + std::vector const &GetSortedFiles() const { return SortedFiles; } + std::vector const &GetUnsortedFiles() const { return UnsortedFiles; } + + void ProcessIntoVolume(Scanner const &s) + { + gdcm::Scanner::ValuesType vt1 = s.GetValues(gdcm::t1); + for ( + gdcm::Scanner::ValuesType::const_iterator it = vt1.begin(); it != vt1.end(); ++it) + { + ProcessAStudy(s, it->c_str()); + } + } + }; + +} // namespace gdcm + +#endif // DISCRIMINATE_VOLUME_H \ No newline at end of file diff --git a/packages/dicom/gdcm/image-sets-normalization.cxx b/packages/dicom/gdcm/image-sets-normalization.cxx new file mode 100644 index 000000000..d32b2f5e0 --- /dev/null +++ b/packages/dicom/gdcm/image-sets-normalization.cxx @@ -0,0 +1,765 @@ +/*========================================================================= + * + * 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: GDCM (Grassroots DICOM). A DICOM library + + Copyright (c) 2006-2011 Mathieu Malaterre + All rights reserved. + See Copyright.txt or http://gdcm.sourceforge.net/Copyright.html 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 + +#include "rapidjson/document.h" +#include "rapidjson/stringbuffer.h" +#include "rapidjson/writer.h" + +#include "gdcmGlobal.h" +#include "gdcmDicts.h" +#include "gdcmImageReader.h" + +#include "itksys/SystemTools.hxx" +#include "itksys/Base64.h" +#include "itkMakeUniqueForOverwrite.h" + +#include "itkPipeline.h" +#include "itkInputTextStream.h" +#include "itkOutputTextStream.h" + +#include "CharStringToUTF8Converter.h" +#include "Tags.h" +#include "TagsOptionParser.h" +#include "SortSpatially.h" + +const Tags SERIES_GROUP_BY_DEFAULT = Tags{SERIES_UID, FRAME_OF_REFERENCE_UID}; +const Tags IMAGE_SET_GROUP_BY_DEFAULT = Tags{STUDY_UID}; + + +std::string getLabelFromTag(const gdcm::Tag &tag, const gdcm::DataSet &dataSet) +{ + if (tag.IsPrivateCreator()) + { + return tag.PrintAsContinuousUpperCaseString(); + } + std::string strowner; + const char *owner = 0; + if (tag.IsPrivate() && !tag.IsPrivateCreator()) + { + strowner = dataSet.GetPrivateCreator(tag); + owner = strowner.c_str(); + } + const gdcm::Global &g = gdcm::Global::GetInstance(); + const gdcm::Dicts &dicts = g.GetDicts(); + const gdcm::DictEntry &entry = dicts.GetDictEntry(tag, owner); + const std::string keyword = entry.GetKeyword(); + if (keyword.empty()) + { + // There are empty keywords in test/data/input/dicom-images/MR files + return tag.PrintAsContinuousUpperCaseString(); + } + return keyword; +} + +namespace gdcm +{ + + inline bool canContainBackslash(const VR::VRType vrType) + { + assert(VR::IsASCII(vrType)); + // PS 3.5-2011 / Table 6.2-1 DICOM VALUE REPRESENTATIONS + switch (vrType) + { + case VR::AE: // ScheduledStationAETitle + // case VR::AS: // no + // case VR::AT: // binary + case VR::CS: // SpecificCharacterSet + case VR::DA: // CalibrationDate + case VR::DS: // FrameTimeVector + case VR::DT: // ReferencedDateTime + // case VR::FD: // binary + // case VR::FL: + case VR::IS: // ReferencedFrameNumber + case VR::LO: // OtherPatientIDs + // case VR::LT: // VM1 + // case VR::OB: // binary + // case VR::OD: // binary + // case VR::OF: // binary + // case VR::OW: // binary + case VR::PN: // PerformingPhysicianName + case VR::SH: // PatientTelephoneNumbers + // case VR::SL: // binary + // case VR::SQ: // binary + // case VR::SS: // binary + // case VR::ST: // VM1 + case VR::TM: // CalibrationTime + case VR::UI: // SOPClassesInStudy + // case VR::UL: // binary + // case VR::UN: // binary + // case VR::US: // binary + // case VR::UT: // VM1 + assert(!(vrType & VR::VR_VM1)); + return true; + default:; + } + return false; + } + + void dataElementToJSONArray(const VR::VRType vr, const DataElement &de, rapidjson::Value &jsonArray, const CharStringToUTF8Converter toUtf8, rapidjson::Document::AllocatorType &allocator) + { + jsonArray.SetArray(); + if (de.IsEmpty()) + { + // F.2.5 DICOM JSON Model Null Values + if (vr == VR::PN) + { + jsonArray.PushBack(rapidjson::Value(rapidjson::kObjectType), allocator); + } + return; + } + const bool checkbackslash = canContainBackslash(vr); + const ByteValue *bv = de.GetByteValue(); + const char *value = bv->GetPointer(); + size_t len = bv->GetLength(); + + if (vr == VR::UI) + { + const std::string strui(value, len); + const size_t lenuid = strlen(strui.c_str()); // trick to remove trailing \0 + rapidjson::Value stringValue; + stringValue.SetString(strui.c_str(), lenuid, allocator); + jsonArray.PushBack(stringValue, allocator); + } + else if (vr == VR::PN) + { + const char *str1 = value; + // remove whitespace: + while (str1[len - 1] == ' ') + { + len--; + } + assert(str1); + std::stringstream ss; + // static const char *Keys[] = { + // "Alphabetic", + // "Ideographic", + // "Phonetic", + // }; + while (1) + { + assert(str1 && (size_t)(str1 - value) <= len); + const char *sep = strchr(str1, '\\'); + const size_t llen = (sep != NULL) ? (sep - str1) : (value + len - str1); + const std::string component(str1, llen); + + const char *str2 = component.c_str(); + assert(str2); + const size_t len2 = component.size(); + assert(len2 == llen); + + int idx = 0; + // Just get Alphabetic name, hence the comments and extra breaks + // rapidjson::Value namesObject(rapidjson::kObjectType); + rapidjson::Value name; + while (1) + { + assert(str2 && (size_t)(str2 - component.c_str()) <= len2); + const char *sep2 = strchr(str2, '='); + const size_t llen2 = (sep2 != NULL) ? (sep2 - str2) : (component.c_str() + len2 - str2); + const std::string group = toUtf8.convertCharStringToUTF8(str2, llen2); + // const char *thekey = Keys[idx++]; + + // rapidjson::Value nameType(thekey, allocator); + name.SetString(group.c_str(), group.size(), allocator); + + // namesObject.AddMember(nameType, name, allocator); + break; // just Alphabetic, short circuit + // if (sep2 == NULL) + // break; + // str2 = sep2 + 1; + } + // jsonArray.PushBack(namesObject, allocator); + jsonArray.PushBack(name, allocator); + break; // just Alphabetic, short circuit + if (sep == NULL) + break; + str1 = sep + 1; + assert(checkbackslash); + } + } + else if (vr == VR::DS || vr == VR::IS) + { + const char *str1 = value; + assert(str1); + VRToType::Type vris; + VRToType::Type vrds; + while (1) + { + std::stringstream ss; + assert(str1 && (size_t)(str1 - value) <= len); + const char *sep = strchr(str1, '\\'); + const size_t llen = (sep != NULL) ? (sep - str1) : (value + len - str1); + // This is complex, IS/DS should not be stored as string anymore + switch (vr) + { + case VR::IS: + ss.str(std::string(str1, llen)); + ss >> vris; + jsonArray.PushBack(rapidjson::Value(vris), allocator); + break; + case VR::DS: + ss.str(std::string(str1, llen)); + ss >> vrds; + jsonArray.PushBack(rapidjson::Value(vrds), allocator); + break; + default: + assert(0); // programmer error + } + if (sep == NULL) + break; + str1 = sep + 1; + assert(checkbackslash); + } + } + else if (checkbackslash) + { + const char *str1 = value; + assert(str1); + while (1) + { + assert(str1 && (size_t)(str1 - value) <= len); + const char *sep = strchr(str1, '\\'); + const size_t llen = (sep != NULL) ? (sep - str1) : (value + len - str1); + std::string valueUtf8 = toUtf8.convertCharStringToUTF8(str1, llen); + // Trim trailing space if exists + if (!valueUtf8.empty() && valueUtf8.back() == ' ') + { + valueUtf8.pop_back(); + } + rapidjson::Value valueString; + valueString.SetString(valueUtf8.c_str(), valueUtf8.size(), allocator); + jsonArray.PushBack(valueString, allocator); + if (sep == NULL) + break; + str1 = sep + 1; + } + } + else // default + { + std::string valueUtf8 = toUtf8.convertCharStringToUTF8(value, len); + // Trim trailing space if exists + if (!valueUtf8.empty() && valueUtf8.back() == ' ') + { + valueUtf8.pop_back(); + } + rapidjson::Value valueString; + valueString.SetString(valueUtf8.c_str(), valueUtf8.size(), allocator); + jsonArray.PushBack(valueString, allocator); + } + } + + rapidjson::Value *toJson(const gdcm::DataSet &dataSet, const Tags &pickTags, const Tags &skipTags, const CharStringToUTF8Converter &toUtf8, rapidjson::Value &dicomTagsObject, rapidjson::Document::AllocatorType &allocator) + { + for (gdcm::DataSet::ConstIterator it = dataSet.Begin(); it != dataSet.End(); ++it) + { + const gdcm::DataElement &de = *it; + VR::VRType vr = de.GetVR(); + const gdcm::Tag &t = de.GetTag(); + if (t.IsGroupLength() || t == PIXEL_DATA_TAG || skipTags.find(t) != skipTags.end()) + continue; // skip useless group length and pixel data tag + if (!pickTags.empty() && pickTags.find(t) == pickTags.end()) + continue; // skip tags that are not in the pick list if it has any + + const bool isSequence = vr == VR::SQ || de.IsUndefinedLength(); + const bool isPrivateCreator = t.IsPrivateCreator(); + if (isSequence) + vr = VR::SQ; + else if (isPrivateCreator) + vr = VR::LO; // always prefer VR::LO (over invalid/UN) + else if (vr == VR::INVALID) + vr = VR::UN; + const char *vr_str = VR::GetVRString(vr); + assert(VR::GetVRTypeFromFile(vr_str) != VR::INVALID); + + rapidjson::Value tagValue; + + if (vr == VR::SQ) + { + // Sequence Value Representations are nested datasets + SmartPointer sqi; + sqi = de.GetValueAsSQ(); + if (sqi) + { + tagValue.SetArray(); + int nitems = sqi->GetNumberOfItems(); + for (int i = 1; i <= nitems; ++i) + { + const Item &item = sqi->GetItem(i); + const DataSet &nested = item.GetNestedDataSet(); + rapidjson::Value sequenceObject(rapidjson::kObjectType); + // grab all nested tags, empty pick and skip tag sets + toJson(nested, EMPTY_TAGS, EMPTY_TAGS, toUtf8, sequenceObject, allocator); + tagValue.PushBack(sequenceObject, allocator); + } + } + + // Strange code from gdcmJSON.cxx + // else if (const SequenceOfFragments *sqf = de.GetSequenceOfFragments()) + // { + // tagValue.SetNull(); // FIXME + // assert(0); + // } + // else + // { + // assert(de.IsEmpty()); + // // json_object_array_add(my_array, NULL ); // F.2.5 req ? + // } + } + else if (VR::IsASCII(vr)) + { + dataElementToJSONArray(vr, de, tagValue, toUtf8, allocator); + } + else + { + tagValue.SetArray(); + + switch (vr) + { + case VR::FD: + { + Element el; + el.Set(de.GetValue()); + int ellen = el.GetLength(); + for (int i = 0; i < ellen; ++i) + { + rapidjson::Value elValue; + elValue.SetDouble(el.GetValue(i)); + tagValue.PushBack(elValue, allocator); + } + } + break; + case VR::FL: + { + Element el; + el.Set(de.GetValue()); + int ellen = el.GetLength(); + for (int i = 0; i < ellen; ++i) + { + rapidjson::Value elValue; + elValue.SetFloat(el.GetValue(i)); + tagValue.PushBack(elValue, allocator); + } + } + break; + case VR::SS: + { + Element el; + el.Set(de.GetValue()); + int ellen = el.GetLength(); + for (int i = 0; i < ellen; ++i) + { + rapidjson::Value elValue; + elValue.SetInt(el.GetValue(i)); + tagValue.PushBack(elValue, allocator); + } + } + break; + case VR::US: + { + Element el; + el.Set(de.GetValue()); + int ellen = el.GetLength(); + for (int i = 0; i < ellen; ++i) + { + rapidjson::Value elValue; + elValue.SetUint(el.GetValue(i)); + tagValue.PushBack(elValue, allocator); + } + } + break; + case VR::SL: + { + Element el; + el.Set(de.GetValue()); + int ellen = el.GetLength(); + for (int i = 0; i < ellen; ++i) + { + rapidjson::Value elValue; + elValue.SetInt(el.GetValue(i)); + tagValue.PushBack(elValue, allocator); + } + } + break; + case VR::UL: + { + Element el; + el.Set(de.GetValue()); + int ellen = el.GetLength(); + for (int i = 0; i < ellen; ++i) + { + rapidjson::Value elValue; + elValue.SetUint(el.GetValue(i)); + tagValue.PushBack(elValue, allocator); + } + } + break; + case VR::AT: + { + Element el; + el.Set(de.GetValue()); + int ellen = el.GetLength(); + for (int i = 0; i < ellen; ++i) + { + const std::string atstr = el.GetValue(i).PrintAsContinuousUpperCaseString(); + rapidjson::Value jsonElement; + jsonElement.SetString(atstr.c_str(), atstr.size(), allocator); + tagValue.PushBack(jsonElement, allocator); + } + } + break; + case VR::UN: + case VR::INVALID: + case VR::OD: + case VR::OF: + case VR::OB: + case VR::OW: + { + assert(!de.IsUndefinedLength()); // handled before + const gdcm::ByteValue *bv = de.GetByteValue(); + if (bv) + { + // base64 streams have to be a multiple of 4 bytes in length + int encodedLengthEstimate = 2 * bv->GetLength(); + encodedLengthEstimate = ((encodedLengthEstimate / 4) + 1) * 4; + + const auto bin = itk::make_unique_for_overwrite(encodedLengthEstimate); + auto encodedLengthActual = + static_cast(itksysBase64_Encode((const unsigned char *)bv->GetPointer(), + static_cast(bv->GetLength()), + (unsigned char *)bin.get(), + 0)); + std::string encodedValue(bin.get(), encodedLengthActual); + tagValue.SetString(encodedValue.c_str(), encodedValue.size(), allocator); + } + } + break; + default: + assert(0); // programmer error + } // end switch + } // end array else + + if (tagValue.IsArray()) + { + int arraySize = tagValue.Size(); + if (arraySize == 0) + { + continue; // skip empty arrays + } + else if (arraySize == 1) + { + // Unwrap array of size 1 + tagValue = tagValue[0]; // different from gdcmJSON.cxx + } + } + + const std::string &label = getLabelFromTag(t, dataSet); + rapidjson::Value tagName; + tagName.SetString(label.c_str(), label.size(), allocator); + dicomTagsObject.AddMember(tagName, tagValue, allocator); + } + return &dicomTagsObject; + } +} + +rapidjson::Value *toJson(const gdcm::DataSet &dataSet, const Tags &pickTags, const Tags &skipTags, rapidjson::Value &dicomTagsObject, rapidjson::Document::AllocatorType &allocator) +{ + const auto specificCharacterSet = getTagBuffer(dataSet, SPECIFIC_CHARACTER_SET); + const std::string charSet = specificCharacterSet.first == nullptr ? "" : std::string(specificCharacterSet.first, specificCharacterSet.second); + const CharStringToUTF8Converter decoder = CharStringToUTF8Converter(charSet); + return toJson(dataSet, pickTags, skipTags, decoder, dicomTagsObject, allocator); +} + +using FileName = std::string; + +struct DicomFile +{ + FileName fileName; + gdcm::DataSet dataSet; + + DicomFile(const FileName &fileName) + : fileName(fileName) + { + gdcm::ImageReader reader; + reader.SetFileName(fileName.c_str()); + if (!reader.Read()) + { + throw std::runtime_error("Failed to read the input DICOM file: " + fileName); + } + const gdcm::File &f = reader.GetFile(); + dataSet = f.GetDataSet(); + } + + bool operator==(const DicomFile &other) const + { + return fileName == other.fileName; + } +}; + +struct dicomFileHash +{ + std::size_t operator()(const DicomFile &dicomFile) const + { + return std::hash{}(dicomFile.fileName); + } +}; +using DicomFiles = std::unordered_set; + +DicomFiles loadFiles(const std::vector &fileNames) +{ + DicomFiles dicomFiles; + for (const FileName &fileName : fileNames) + { + dicomFiles.insert(DicomFile(fileName)); + } + return dicomFiles; +} + +using Volume = std::vector; +using Volumes = std::vector; // Aka ImageSet. A set of volumes/series that share Study and Patient. +using ImageSets = std::vector; + +bool compareTags(const gdcm::DataSet &tagsA, const gdcm::DataSet &tagsB, const Tags &tagKeys) +{ + for (const auto &tagKey : tagKeys) + { + const auto tagA = getTagBuffer(tagsA, tagKey); + const auto tagB = getTagBuffer(tagsB, tagKey); + if (tagA.first == nullptr || tagB.first == nullptr) + { + return false; + } + if (std::memcmp(tagA.first, tagB.first, tagB.second) != 0) + { + return false; + } + } + return true; +} + +bool isSameVolume(const gdcm::DataSet &tagsA, const gdcm::DataSet &tagsB, const Tags &criteria) +{ + return compareTags(tagsA, tagsB, criteria); +} + +Volumes groupByVolume(const DicomFiles &dicomFiles, const Tags &criteria = {SERIES_UID, FRAME_OF_REFERENCE_UID}) +{ + Volumes volumes; + for (const DicomFile &dicomFile : dicomFiles) + { + const auto candidate = dicomFile.dataSet; + auto matchingVolume = std::find_if(volumes.begin(), volumes.end(), [&candidate, &criteria](const Volume &volume) + { return isSameVolume(volume.begin()->dataSet, candidate, criteria); }); + + if (matchingVolume != volumes.end()) + { + matchingVolume->push_back(dicomFile); + } + else + { + Volume newVolume({dicomFile}); + volumes.push_back(newVolume); + } + } + return volumes; +} + +ImageSets groupByImageSet(const Volumes &volumes, const Tags &imageSetCriteria = {STUDY_UID}) +{ + ImageSets imageSets; + for (const Volume &volume : volumes) + { + const gdcm::DataSet volumeDataSet = volume.begin()->dataSet; + auto matchingImageSet = std::find_if(imageSets.begin(), imageSets.end(), [&volumeDataSet, &imageSetCriteria](const Volumes &volumes) + { + const gdcm::DataSet imageSetDataSet = volumes.begin()->begin()->dataSet; + return compareTags(volumeDataSet, imageSetDataSet, imageSetCriteria); }); + if (matchingImageSet != imageSets.end()) + { + matchingImageSet->push_back(volume); + } + else + { + Volumes newImageSet({volume}); + imageSets.push_back(newImageSet); + } + } + return imageSets; +} + +Volumes sortSpatially(Volumes &volumes) +{ + Volumes sortedVolumes; + for (Volume &volume : volumes) + { + std::vector unsortedSeriesFileNames; + for (const DicomFile &dicomFile : volume) + { + unsortedSeriesFileNames.push_back(dicomFile.fileName); + } + std::vector sortedFileNames = sortSpatially(unsortedSeriesFileNames); + + Volume sorted; + for (const auto &fileName : sortedFileNames) + { + const auto matchingDicomFile = std::find_if(volume.begin(), volume.end(), [&fileName](const DicomFile &dicomFile) + { return dicomFile.fileName == fileName; }); + if (matchingDicomFile != volume.end()) + { + sorted.push_back(*matchingDicomFile); + } + } + sortedVolumes.push_back(sorted); + } + return sortedVolumes; +} + +std::string getUID(const gdcm::DataSet &ds, const Tag &tag) +{ + if (!ds.FindDataElement(tag) || ds.GetDataElement(tag).IsEmpty()) + { + throw std::runtime_error("Tag not found"); + } + const gdcm::DataElement de = ds.GetDataElement(tag); + const gdcm::ByteValue *bv = de.GetByteValue(); + const char *tagValue = bv->GetPointer(); + size_t len = bv->GetLength(); + return std::string(tagValue, len); +} + +rapidjson::Document toJson(const ImageSets &imageSets) +{ + rapidjson::Document imageSetsJson(rapidjson::kArrayType); + rapidjson::Document::AllocatorType &allocator = imageSetsJson.GetAllocator(); + for (const Volumes &volumes : imageSets) + { + gdcm::DataSet dataSet; + rapidjson::Value seriesById(rapidjson::kObjectType); + for (const Volume &volume : volumes) + { + rapidjson::Value instances(rapidjson::kObjectType); + for (const auto &dicomFile : volume) + { + FileName file = dicomFile.fileName; + dataSet = dicomFile.dataSet; + rapidjson::Value instanceTagsJson(rapidjson::kObjectType); + + toJson(dataSet, EMPTY_TAGS, NON_INSTANCE, instanceTagsJson, allocator); + rapidjson::Value instance(rapidjson::kObjectType); + instance.AddMember("DICOM", instanceTagsJson, allocator); + + rapidjson::Value fileNameValue; + fileNameValue.SetString(file.c_str(), file.size(), allocator); + rapidjson::Value imageFrame(rapidjson::kObjectType); + imageFrame.AddMember("ID", fileNameValue, allocator); + rapidjson::Value imageFrames(rapidjson::kArrayType); + imageFrames.PushBack(imageFrame, allocator); + instance.AddMember("ImageFrames", imageFrames, allocator); + + // instance by UID under instances + const std::string instanceUID = getUID(dataSet, INSTANCE_UID); + rapidjson::Value instanceId; + instanceId.SetString(instanceUID.c_str(), instanceUID.size(), allocator); + instances.AddMember(instanceId, instance, allocator); + } + + // Series + rapidjson::Value seriesTags(rapidjson::kObjectType); + toJson(dataSet, SERIES_TAGS, EMPTY_TAGS, seriesTags, allocator); + rapidjson::Value series(rapidjson::kObjectType); + series.AddMember("DICOM", seriesTags, allocator); + series.AddMember("Instances", instances, allocator); + + int volumeIndex = std::distance(volumes.begin(), std::find(volumes.begin(), volumes.end(), volume)); + const std::string seriesId = getUID(dataSet, SERIES_UID) + '.' + std::to_string(volumeIndex); + + rapidjson::Value seriesIdJson; + seriesIdJson.SetString(seriesId.c_str(), seriesId.size(), allocator); + seriesById.AddMember(seriesIdJson, series, allocator); + } + + rapidjson::Value imageSet(rapidjson::kObjectType); + + // Patient + rapidjson::Value patientTags(rapidjson::kObjectType); + toJson(dataSet, PATIENT_TAGS, EMPTY_TAGS, patientTags, allocator); + rapidjson::Value patient(rapidjson::kObjectType); + patient.AddMember("DICOM", patientTags, allocator); + imageSet.AddMember("Patient", patient, allocator); + + // Study + rapidjson::Value studyTags(rapidjson::kObjectType); + toJson(dataSet, STUDY_TAGS, EMPTY_TAGS, studyTags, allocator); + rapidjson::Value study(rapidjson::kObjectType); + study.AddMember("DICOM", studyTags, allocator); + study.AddMember("Series", seriesById, allocator); + imageSet.AddMember("Study", study, allocator); + + imageSetsJson.PushBack(imageSet, allocator); + } + return imageSetsJson; +} + +int main(int argc, char *argv[]) +{ + itk::wasm::Pipeline pipeline("image-sets-normalization", "Group DICOM files into image sets", argc, argv); + + std::vector files; + pipeline.add_option("--files", files, "DICOM files")->required()->check(CLI::ExistingFile)->type_size(1, -1)->type_name("INPUT_BINARY_FILE"); + + itk::wasm::InputTextStream seriesGroupByOption; + pipeline.add_option("--series-group-by", seriesGroupByOption, "Create series so that all instances in a series share these tags. Option is a JSON object with a \"tags\" array. Example tag: \"0008|103e\". If not provided, defaults to Series UID and Frame Of Reference UID tags.")->type_name("INPUT_JSON"); + itk::wasm::InputTextStream imageSetGroupByOption; + pipeline.add_option("--image-set-group-by", imageSetGroupByOption, "Create image sets so that all series in a set share these tags. Option is a JSON object with a \"tags\" array. Example tag: \"0008|103e\". If not provided, defaults to Study UID tag.")->type_name("INPUT_JSON"); + + itk::wasm::OutputTextStream imageSetsOutput; + pipeline.add_option("image-sets", imageSetsOutput, "Image sets JSON")->required()->type_name("OUTPUT_JSON"); + + ITK_WASM_PARSE(pipeline); + + const std::optional seriesGroupByParse = parseTags(seriesGroupByOption, pipeline); + const Tags seriesGroupBy = seriesGroupByParse.value_or(SERIES_GROUP_BY_DEFAULT); + const std::optional imageSetGroupByParse = parseTags(imageSetGroupByOption, pipeline); + const Tags imageSetGroupBy = imageSetGroupByParse.value_or(IMAGE_SET_GROUP_BY_DEFAULT); + + const DicomFiles dicomFiles = loadFiles(files); + Volumes volumes = groupByVolume(dicomFiles, seriesGroupBy); + volumes = sortSpatially(volumes); + const ImageSets imageSets = groupByImageSet(volumes, imageSetGroupBy); + + rapidjson::Document imageSetsJson = toJson(imageSets); + rapidjson::StringBuffer stringBuffer; + rapidjson::Writer writer(stringBuffer); + imageSetsJson.Accept(writer); + imageSetsOutput.Get() << stringBuffer.GetString(); + + return EXIT_SUCCESS; +} diff --git a/packages/dicom/gdcm/read-dicom-tags.cxx b/packages/dicom/gdcm/read-dicom-tags.cxx index cf6d7fc55..de9cda83d 100644 --- a/packages/dicom/gdcm/read-dicom-tags.cxx +++ b/packages/dicom/gdcm/read-dicom-tags.cxx @@ -41,543 +41,7 @@ #include "itkInputTextStream.h" #include "itkOutputTextStream.h" -const std::string DEFAULT_ENCODING("ISO_IR 6"); -const std::string DEFAULT_ISO_2022_ENCODING("ISO 2022 IR 6"); -constexpr const char * ASCII = "ASCII"; - -// delimiters: CR, LF, FF, ESC, TAB (see -// https://dicom.nema.org/medical/dicom/current/output/html/part05.html#sect_6.1.3, -// table 6.1-1) -// Also includes 05/12 (BACKSLASH in IR 13 or YEN SIGN in IR 14), since that -// separates Data Element Values and it resets to initial charset. -// See: dicom part 5, sect 6.1.2.5.3 -constexpr const char * DEFAULT_DELIMS = "\x1b\x09\x0a\x0c\x0d\x5c"; -// DEFAULT_DELIMS + "^" and "=" -constexpr const char * PATIENT_NAME_DELIMS = "\x1b\x09\x0a\x0c\x0d\x5c^="; - -std::string -unpackMetaAsString(const itk::MetaDataObjectBase::Pointer & metaValue) -{ - using MetaDataStringType = itk::MetaDataObject; - MetaDataStringType::Pointer value = dynamic_cast(metaValue.GetPointer()); - if (value != nullptr) - { - return value->GetMetaDataObjectValue(); - } - return {}; -} - -// If not found, then pos == len -size_t -findDelim(const char * str, size_t len, size_t pos = 0, const char * delims = DEFAULT_DELIMS) -{ - while (pos < len && strchr(delims, str[pos]) == nullptr) - { - ++pos; - } - return pos; -} - -std::string -trimWhitespace(const std::string & term) -{ - auto start = term.begin(); - auto end = term.end(); - - while (start != end && std::isspace(*start)) - { - ++start; - } - - // need to --end once before checking isspace - do - { - --end; - } while (end != start && std::isspace(*end)); - - return std::string(start, end + 1); -} - -std::string -normalizeTerm(const std::string & term) -{ - return trimWhitespace(term); -} - -const char * -definedTermToIconvCharset(const std::string & defTerm) -{ - // be strict about comparing defined terms, so no fancy parsing - // that could possibly make these operations faster. - // See: - // https://dicom.nema.org/medical/dicom/current/output/chtml/part02/sect_D.6.2.html - if (defTerm == "ISO_IR 6" || defTerm == "ISO 2022 IR 6") - { - return ASCII; - } - if (defTerm == "ISO_IR 100" || defTerm == "ISO 2022 IR 100") - { - return "ISO-8859-1"; // Latin 1 - } - if (defTerm == "ISO_IR 101" || defTerm == "ISO 2022 IR 101") - { - return "ISO-8859-2"; // Latin 2 - } - if (defTerm == "ISO_IR 109" || defTerm == "ISO 2022 IR 109") - { - return "ISO-8859-3"; // Latin 3 - } - if (defTerm == "ISO_IR 110" || defTerm == "ISO 2022 IR 110") - { - return "ISO-8859-4"; // Latin 4 - } - if (defTerm == "ISO_IR 144" || defTerm == "ISO 2022 IR 144") - { - return "ISO-8859-5"; // Cyrillic - } - if (defTerm == "ISO_IR 127" || defTerm == "ISO 2022 IR 127") - { - return "ISO-8859-6"; // Arabic - } - if (defTerm == "ISO_IR 126" || defTerm == "ISO 2022 IR 126") - { - return "ISO-8859-7"; // Greek - } - if (defTerm == "ISO_IR 138" || defTerm == "ISO 2022 IR 138") - { - return "ISO-8859-8"; // Hebrew - } - if (defTerm == "ISO_IR 148" || defTerm == "ISO 2022 IR 148") - { - return "ISO-8859-9"; // Latin 5, Turkish - } - if (defTerm == "ISO_IR 13" || defTerm == "ISO 2022 IR 13") - { - // while technically not strict, SHIFT_JIS succeeds JIS X 0201 - // See: https://en.wikipedia.org/wiki/JIS_X_0201 - return "SHIFT_JIS"; // Japanese - } - if (defTerm == "ISO_IR 166" || defTerm == "ISO 2022 IR 166") - { - return "TIS-620"; // Thai - } - if (defTerm == "ISO 2022 IR 87") - { - // see: https://en.wikipedia.org/wiki/JIS_X_0208 - return "ISO-2022-JP"; // Japanese - } - if (defTerm == "ISO 2022 IR 159") - { - // see: https://en.wikipedia.org/wiki/JIS_X_0212 - return "ISO-2022-JP-1"; // Japanese - } - if (defTerm == "ISO 2022 IR 149") - { - return "EUC-KR"; // Korean - } - if (defTerm == "ISO 2022 IR 58") - { - return "EUC-CN"; // Chinese - } - if (defTerm == "ISO_IR 192") - { - return "UTF-8"; - } - if (defTerm == "GB18030") - { - return "GB18030"; - } - if (defTerm == "GBK") - { - return "GBK"; - } - return nullptr; -} - -// seq should be the sequence after the ESC char -// return value should match in definedTermToIconvCharset -const char * -iso2022EscSelectCharset(const char * seq) -{ - if (seq[0] == '(' && seq[1] == 'B') - { - return "ISO 2022 IR 6"; - } - if (seq[0] == '-' && seq[1] == 'A') - { - return "ISO 2022 IR 100"; - } - if (seq[0] == '-' && seq[1] == 'B') - { - return "ISO 2022 IR 101"; - } - if (seq[0] == '-' && seq[1] == 'C') - { - return "ISO 2022 IR 109"; - } - if (seq[0] == '-' && seq[1] == 'D') - { - return "ISO 2022 IR 110"; - } - if (seq[0] == '-' && seq[1] == 'L') - { - return "ISO 2022 IR 144"; - } - if (seq[0] == '-' && seq[1] == 'G') - { - return "ISO 2022 IR 127"; - } - if (seq[0] == '-' && seq[1] == 'F') - { - return "ISO 2022 IR 126"; - } - if (seq[0] == '-' && seq[1] == 'H') - { - return "ISO 2022 IR 138"; - } - if (seq[0] == '-' && seq[1] == 'M') - { - return "ISO 2022 IR 148"; - } - // technically 'J' corresponds to IR 14, byt SHIFT_JIS should still work - if (seq[0] == '-' && (seq[1] == 'I' || seq[1] == 'J')) - { - return "ISO 2022 IR 13"; - } - if (seq[0] == '-' && seq[1] == 'T') - { - return "ISO 2022 IR 166"; - } - if (seq[0] == '$' && seq[1] == 'B') - { - return "ISO 2022 IR 87"; - } - if (seq[0] == '$' && seq[1] == '(' && seq[2] == 'D') - { - return "ISO 2022 IR 159"; - } - if (seq[0] == '$' && seq[1] == ')' && seq[2] == 'C') - { - return "ISO 2022 IR 149"; - } - if (seq[0] == '$' && seq[1] == ')' && seq[2] == 'A') - { - return "ISO 2022 IR 58"; - } - if ((seq[0] == ')' && seq[1] == 'I') || (seq[0] == '(' && seq[1] == 'J')) - { - return "ISO 2022 IR 13"; - } - return ""; -} - -// seq should point after the ESC char. Returned length will -// not include ESC char. -size_t -iso2022EscSeqLength(const char * seq) -{ - if (seq[0] == '$' && seq[1] >= '(' && seq[1] <= '/') - { - return 3; - } - return 2; -} - -class CharStringToUTF8Converter -{ -public: - // See: setSpecificCharacterSet(const char *) - CharStringToUTF8Converter(const std::string & spcharsets) - : CharStringToUTF8Converter(spcharsets.c_str()) - {} - CharStringToUTF8Converter(const char * spcharsets) - : handlePatientName(false) - { - this->setSpecificCharacterSet(spcharsets); - }; - - /** - * Input must be the DICOM SpecificCharacterSet element value. - * See: - * https://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.12.1.1.2 - */ - void - setSpecificCharacterSet(const char * spcharsets) - { - std::string specificCharacterSet(spcharsets); - std::string token; - std::istringstream tokStream(specificCharacterSet); - - m_charsets.clear(); - - int count = 0; - while (std::getline(tokStream, token, '\\')) - { - token = normalizeTerm(token); - - // case: first element is empty. Use default ISO-IR 6 encoding. - if (token.size() == 0 && count == 0) - { - m_charsets.push_back(DEFAULT_ENCODING); - // "Hack" to handle case where ISO-646 (dicom default encoding) is - // implicitly first in the list. Since we check for charset existence when - // switching charsets as per ISO 2022, we put both regular and ISO 2022 - // names for the default encoding. - m_charsets.push_back(DEFAULT_ISO_2022_ENCODING); - } - else if (m_charsets.end() == std::find(m_charsets.begin(), m_charsets.end(), token)) - { - // case: no duplicates - const char * chname = definedTermToIconvCharset(token); - // handle charsets that do not allow code extensions - if (count > 0 && (token == "GB18030" || token == "GBK" || token == "ISO_IR 192")) - { - std::cerr << "WARN: charset " << token << " does not support code extensions; ignoring" << std::endl; - } - else if (chname != nullptr && chname != ASCII) - { - // ISO_IR 6 isn't a formally recognized defined term, so use ASCII - // above. - m_charsets.push_back(token); - } - } - else - { - std::cerr << "WARN: Found duplicate charset '" + token + "'; ignoring" << std::endl; - } - ++count; - } - - if (count == 0) - { - // use default encoding - m_charsets.push_back(DEFAULT_ENCODING); - } - - if (m_charsets.size() == 0) - { - std::cerr << "WARN: Found no suitable charsets!" << std::endl; - } - } - - std::string - convertCharStringToUTF8(const std::string & str) - { - size_t len = str.size(); - return this->convertCharStringToUTF8(str.c_str(), len); - } - - std::string - convertCharStringToUTF8(const char * str, size_t len) - { - // m_charsets must always have at least 1 element prior to calling - const char * initialCharset = definedTermToIconvCharset(m_charsets[0]); - if (initialCharset == nullptr) - { - return {}; - } - - iconv_t cd = iconv_open("UTF-8", initialCharset); - if (cd == (iconv_t)-1) - { - return {}; - } - - int utf8len = len * 4; - std::unique_ptr result(new char[utf8len + 1]()); // UTF8 will have max length of utf8len - - // make a copy because iconv requires a char * - char * copiedStr = (char *)malloc(len + 1); - strncpy(copiedStr, str, len); - - char * inbuf = copiedStr; - char * outbuf = result.get(); - size_t inbytesleft = len; - size_t outbytesleft = utf8len; - - // special case: only one charset, so assume string is just that charset. - if (m_charsets.size() == 1) - { - iconv(cd, &inbuf, &inbytesleft, &outbuf, &outbytesleft); - } - else - { - size_t fragmentStart = 0; - size_t fragmentEnd = 0; - - while (fragmentStart < len) - { - const char * delims = this->handlePatientName ? PATIENT_NAME_DELIMS : DEFAULT_DELIMS; - // fragmentEnd will always be end of current fragment (exclusive end) - fragmentEnd = findDelim(str, len, fragmentStart + 1, delims); - inbuf = copiedStr + fragmentStart; - inbytesleft = fragmentEnd - fragmentStart; - - iconv(cd, &inbuf, &inbytesleft, &outbuf, &outbytesleft); - - fragmentStart = fragmentEnd; - bool isEsc = str[fragmentEnd] == 0x1b; - - if (fragmentStart < len) - { - const char * nextCharset; - int seek = 0; - - if (isEsc) - { // case: ISO 2022 escape encountered - const char * escSeq = copiedStr + fragmentStart + 1; - - const char * nextTerm = iso2022EscSelectCharset(escSeq); - nextCharset = definedTermToIconvCharset(std::string(nextTerm)); - if (nextCharset == nullptr || m_charsets.end() == std::find(m_charsets.begin(), m_charsets.end(), nextTerm)) - { - std::cerr << "WARN: bailing because invalid charset: " << nextTerm << std::endl; - break; // bail out - } - - // ISO-2022-JP is a variant on ISO 2022 for japanese, and so - // it defines its own escape sequences. As such, do not skip the - // escape sequences for ISO-2022-JP, so iconv can properly interpret - // them. - if (0 != strcmp("ISO-2022-JP", nextCharset) && 0 != strcmp("ISO-2022-JP-1", nextCharset)) - { - seek = iso2022EscSeqLength(escSeq) + 1; - } - } - else - { // case: hit a CR, LF, or FF - // reset to initial charset - nextCharset = initialCharset; - } - - if (0 != iconv_close(cd)) - { - std::cerr << "WARN: bailing because iconv_close" << std::endl; - break; // bail out - } - cd = iconv_open("UTF-8", nextCharset); - if (cd == (iconv_t)-1) - { - std::cerr << "WARN: bailing because iconv_open" << std::endl; - break; // bail out - } - - fragmentStart += seek; - } - } - } - - free(copiedStr); - iconv_close(cd); - - // since result is filled with NULL bytes, string constructor will figure out - // the correct string ending. - return std::string(result.get()); - } - - bool - getHandlePatientName() - { - return this->handlePatientName; - } - - void - setHandlePatientName(bool yn) - { - this->handlePatientName = yn; - } - -private: - std::vector m_charsets; - bool handlePatientName; -}; - -namespace itk -{ - -/** \class DICOMTagReader - * - * \brief Reads DICOM tags from a DICOM object. - */ -class DICOMTagReader -{ -public: - using MetaDictType = itk::MetaDataDictionary; - using TagMapType = std::map; - - DICOMTagReader() - : m_dirtyCache(true) - { - m_GDCMImageIO = GDCMImageIO::New(); - } - - /** Sets file name. */ - void - SetFileName(const std::string & file) - { - m_fileName = file; - m_GDCMImageIO->SetFileName(file); - m_dirtyCache = true; - } - - /** Verify file can be read. */ - bool - CanReadFile(const std::string & file) - { - return m_GDCMImageIO->CanReadFile(file.c_str()); - } - - std::string - ReadTag(const std::string & tag) - { - - if (m_dirtyCache) - { - m_GDCMImageIO->SetUseStreamedReading(true); - m_GDCMImageIO->ReadImageInformation(); - m_tagDict = m_GDCMImageIO->GetMetaDataDictionary(); - auto specificCharacterSet = unpackMetaAsString(m_tagDict["0008|0005"]); - m_decoder = CharStringToUTF8Converter(specificCharacterSet); - m_dirtyCache = false; - } - - auto value = unpackMetaAsString(m_tagDict[tag]); - return m_decoder.convertCharStringToUTF8(value); - } - - TagMapType - ReadAllTags() - { - - if (m_dirtyCache) - { - m_GDCMImageIO->SetUseStreamedReading(true); - m_GDCMImageIO->ReadImageInformation(); - m_tagDict = m_GDCMImageIO->GetMetaDataDictionary(); - auto specificCharacterSet = unpackMetaAsString(m_tagDict["0008|0005"]); - m_decoder = CharStringToUTF8Converter(specificCharacterSet); - m_dirtyCache = false; - } - - TagMapType allTagsDict; - for (auto it = m_tagDict.Begin(); it != m_tagDict.End(); ++it) - { - auto value = unpackMetaAsString(it->second); - allTagsDict[it->first] = m_decoder.convertCharStringToUTF8(value); - } - - return allTagsDict; - } - -private: - std::string m_fileName; - itk::GDCMImageIO::Pointer m_GDCMImageIO; - MetaDictType m_tagDict; - CharStringToUTF8Converter m_decoder = CharStringToUTF8Converter(""); - bool m_dirtyCache; -}; - -} // end namespace itk +#include "DICOMTagReader.h" int main( int argc, char * argv[] ) { diff --git a/packages/dicom/gdcm/read-image-dicom-file-series.cxx b/packages/dicom/gdcm/read-image-dicom-file-series.cxx index c93e51ea7..0795059f2 100644 --- a/packages/dicom/gdcm/read-image-dicom-file-series.cxx +++ b/packages/dicom/gdcm/read-image-dicom-file-series.cxx @@ -20,6 +20,10 @@ #include #include +#include "rapidjson/document.h" +#include "rapidjson/prettywriter.h" +#include "rapidjson/ostreamwrapper.h" + #include "itkCommonEnums.h" #include "gdcmSerieHelper.h" #include "itkImageIOBase.h" @@ -32,18 +36,7 @@ #include "itkOutputImage.h" #include "itkOutputTextStream.h" -#include "rapidjson/document.h" -#include "rapidjson/prettywriter.h" -#include "rapidjson/ostreamwrapper.h" - -class CustomSerieHelper: public gdcm::SerieHelper -{ -public: - void AddFileName(std::string const &fileName) - { - SerieHelper::AddFileName(fileName); - } -}; +#include "SortSpatially.h" namespace itk { @@ -210,60 +203,7 @@ int runPipeline(itk::wasm::Pipeline & pipeline, std::vector & input if (!singleSortedSeries) { - std::unique_ptr serieHelper(new CustomSerieHelper()); - for (const std::string & fileName: inputFileNames) - { - serieHelper->AddFileName(fileName); - } - serieHelper->SetUseSeriesDetails(true); - // Add the default restrictions to refine the file set into multiple series. - serieHelper->CreateDefaultUniqueSeriesIdentifier(); - using SeriesIdContainer = std::vector; - SeriesIdContainer seriesUIDs; - // Accessing the first serie found (assume there is at least one) - gdcm::FileList * flist = serieHelper->GetFirstSingleSerieUIDFileSet(); - while (flist) - { - if (!flist->empty()) // make sure we have at leat one serie - { - gdcm::File * file = (*flist)[0]; // for example take the first one - - // Create its unique series ID - const std::string id( serieHelper->CreateUniqueSeriesIdentifier(file)); - - seriesUIDs.push_back(id); - } - flist = serieHelper->GetNextSingleSerieUIDFileSet(); - } - - using FileNamesContainer = std::vector; - FileNamesContainer fileNames; - flist = serieHelper->GetFirstSingleSerieUIDFileSet(); - const std::string serie = seriesUIDs[0]; - bool found = false; - while (flist && !found) - { - if (!flist->empty()) // make sure we have at leat one serie - { - gdcm::File * file = (*flist)[0]; // for example take the first one - const std::string id( serieHelper->CreateUniqueSeriesIdentifier(file)); - if (id == serie) - { - found = true; // we found a match - break; - } - } - flist = serieHelper->GetNextSingleSerieUIDFileSet(); - } - serieHelper->OrderFileList(flist); - - gdcm::FileList::iterator it; - for (it = flist->begin(); it != flist->end(); ++it) - { - gdcm::FileWithName * header = *it; - fileNames.push_back(header->filename); - } - + std::vector fileNames = sortSpatially(inputFileNames); reader->SetFileNames(fileNames); } else diff --git a/packages/dicom/python/itkwasm-dicom-emscripten/itkwasm_dicom_emscripten/image_sets_normalization_async.py b/packages/dicom/python/itkwasm-dicom-emscripten/itkwasm_dicom_emscripten/image_sets_normalization_async.py new file mode 100644 index 000000000..feb74cef6 --- /dev/null +++ b/packages/dicom/python/itkwasm-dicom-emscripten/itkwasm_dicom_emscripten/image_sets_normalization_async.py @@ -0,0 +1,64 @@ +# Generated file. To retain edits, remove this comment. + +from pathlib import Path +import os +from typing import Dict, Tuple, Optional, List, Any + +from .js_package import js_package + +from itkwasm.pyodide import ( + to_js, + to_py, + js_resources +) +from itkwasm import ( + InterfaceTypes, + BinaryFile, +) + +async def image_sets_normalization_async( + files: List[os.PathLike] = [], + series_group_by: Optional[Any] = None, + image_set_group_by: Optional[Any] = None, +) -> Any: + """Group DICOM files into image sets + + :param files: DICOM files + :type files: os.PathLike + + :param series_group_by: Create series so that all instances in a series share these tags. Option is a JSON object with a "tags" array. Example tag: "0008|103e". If not provided, defaults to Series UID and Frame Of Reference UID tags. + :type series_group_by: Any + + :param image_set_group_by: Create image sets so that all series in a set share these tags. Option is a JSON object with a "tags" array. Example tag: "0008|103e". If not provided, defaults to Study UID tag. + :type image_set_group_by: Any + + :return: Image sets JSON + :rtype: Any + """ + js_module = await js_package.js_module + web_worker = js_resources.web_worker + + kwargs = {} + if files is not None: + kwargs["files"] = to_js(BinaryFile(files)) + if series_group_by is not None: + kwargs["seriesGroupBy"] = to_js(series_group_by) + if image_set_group_by is not None: + kwargs["imageSetGroupBy"] = to_js(image_set_group_by) + + outputs = await js_module.imageSetsNormalization(webWorker=web_worker, noCopy=True, **kwargs) + + output_web_worker = None + output_list = [] + outputs_object_map = outputs.as_object_map() + for output_name in outputs.object_keys(): + if output_name == 'webWorker': + output_web_worker = outputs_object_map[output_name] + else: + output_list.append(to_py(outputs_object_map[output_name])) + + js_resources.web_worker = output_web_worker + + if len(output_list) == 1: + return output_list[0] + return tuple(output_list) diff --git a/packages/dicom/python/itkwasm-dicom-wasi/itkwasm_dicom_wasi/__init__.py b/packages/dicom/python/itkwasm-dicom-wasi/itkwasm_dicom_wasi/__init__.py index a0d01df4f..8551cb96b 100644 --- a/packages/dicom/python/itkwasm-dicom-wasi/itkwasm_dicom_wasi/__init__.py +++ b/packages/dicom/python/itkwasm-dicom-wasi/itkwasm_dicom_wasi/__init__.py @@ -10,5 +10,6 @@ from .write_segmentation import write_segmentation from .write_overlapping_segmentation import write_overlapping_segmentation from .write_multi_segmentation import write_multi_segmentation +from .image_sets_normalization import image_sets_normalization from ._version import __version__ diff --git a/packages/dicom/python/itkwasm-dicom-wasi/itkwasm_dicom_wasi/image_sets_normalization.py b/packages/dicom/python/itkwasm-dicom-wasi/itkwasm_dicom_wasi/image_sets_normalization.py new file mode 100644 index 000000000..74d9f8d70 --- /dev/null +++ b/packages/dicom/python/itkwasm-dicom-wasi/itkwasm_dicom_wasi/image_sets_normalization.py @@ -0,0 +1,83 @@ +# Generated file. To retain edits, remove this comment. + +from pathlib import Path, PurePosixPath +import os +from typing import Dict, Tuple, Optional, List, Any + +from importlib_resources import files as file_resources + +_pipeline = None + +from itkwasm import ( + InterfaceTypes, + PipelineOutput, + PipelineInput, + Pipeline, + BinaryFile, +) + +def image_sets_normalization( + files: List[os.PathLike] = [], + series_group_by: Optional[Any] = None, + image_set_group_by: Optional[Any] = None, +) -> Any: + """Group DICOM files into image sets + + :param files: DICOM files + :type files: os.PathLike + + :param series_group_by: Create series so that all instances in a series share these tags. Option is a JSON object with a "tags" array. Example tag: "0008|103e". If not provided, defaults to Series UID and Frame Of Reference UID tags. + :type series_group_by: Any + + :param image_set_group_by: Create image sets so that all series in a set share these tags. Option is a JSON object with a "tags" array. Example tag: "0008|103e". If not provided, defaults to Study UID tag. + :type image_set_group_by: Any + + :return: Image sets JSON + :rtype: Any + """ + global _pipeline + if _pipeline is None: + _pipeline = Pipeline(file_resources('itkwasm_dicom_wasi').joinpath(Path('wasm_modules') / Path('image-sets-normalization.wasi.wasm'))) + + pipeline_outputs: List[PipelineOutput] = [ + PipelineOutput(InterfaceTypes.JsonCompatible), + ] + + pipeline_inputs: List[PipelineInput] = [ + ] + + args: List[str] = ['--memory-io',] + # Inputs + # Outputs + image_sets_name = '0' + args.append(image_sets_name) + + # Options + input_count = len(pipeline_inputs) + if len(files) < 1: + raise ValueError('"files" kwarg must have a length > 1') + if len(files) > 0: + args.append('--files') + for value in files: + input_file = str(PurePosixPath(value)) + pipeline_inputs.append(PipelineInput(InterfaceTypes.BinaryFile, BinaryFile(value))) + args.append(input_file) + + if series_group_by is not None: + pipeline_inputs.append(PipelineInput(InterfaceTypes.JsonCompatible, series_group_by)) + args.append('--series-group-by') + args.append(str(input_count)) + input_count += 1 + + if image_set_group_by is not None: + pipeline_inputs.append(PipelineInput(InterfaceTypes.JsonCompatible, image_set_group_by)) + args.append('--image-set-group-by') + args.append(str(input_count)) + input_count += 1 + + + outputs = _pipeline.run(args, pipeline_outputs, pipeline_inputs) + + result = outputs[0].data + return result + diff --git a/packages/dicom/python/itkwasm-dicom-wasi/itkwasm_dicom_wasi/wasm_modules/image-sets-normalization.wasi.wasm b/packages/dicom/python/itkwasm-dicom-wasi/itkwasm_dicom_wasi/wasm_modules/image-sets-normalization.wasi.wasm new file mode 100755 index 000000000..212f2bf95 Binary files /dev/null and b/packages/dicom/python/itkwasm-dicom-wasi/itkwasm_dicom_wasi/wasm_modules/image-sets-normalization.wasi.wasm differ diff --git a/packages/dicom/python/itkwasm-dicom-wasi/tests/test_image_sets_normalization.py b/packages/dicom/python/itkwasm-dicom-wasi/tests/test_image_sets_normalization.py new file mode 100644 index 000000000..a34d956c5 --- /dev/null +++ b/packages/dicom/python/itkwasm-dicom-wasi/tests/test_image_sets_normalization.py @@ -0,0 +1,115 @@ +from itkwasm_dicom_wasi import image_sets_normalization + +from .common import test_input_path, test_output_path + + +orientation_series = [ + test_input_path / "DicomImageOrientationTest" / "ImageOrientation.1.dcm", + test_input_path / "DicomImageOrientationTest" / "ImageOrientation.2.dcm", + test_input_path / "DicomImageOrientationTest" / "ImageOrientation.3.dcm", +] + +mr_series = [ + test_input_path / "dicom-images" / "MR" / "1-001.dcm", + test_input_path / "dicom-images" / "MR" / "1-002.dcm", + test_input_path / "dicom-images" / "MR" / "1-003.dcm", + test_input_path / "dicom-images" / "MR" / "1-004.dcm", + test_input_path / "dicom-images" / "MR" / "1-005.dcm", +] + +ct_series = [ + test_input_path / "dicom-images" / "CT" / "1-1.dcm", + test_input_path / "dicom-images" / "CT" / "1-2.dcm", +] + + +def pick_files(image_set): + instances = list(image_set["Study"]["Series"].values())[0]["Instances"].values() + files = [instance["ImageFrames"][0]["ID"] for instance in instances] + return files + + +def assert_equal(fileStrings, paths): + assert all(file == str(path) for file, path in zip(fileStrings, paths)) + + +def test_one_series(): + assert orientation_series[0].exists() + out_of_order = [ + orientation_series[1], + orientation_series[2], + orientation_series[0], + ] + image_sets = image_sets_normalization(out_of_order) + assert image_sets + sorted_files = pick_files(image_sets[0]) + assert_equal(sorted_files, orientation_series) + + +def test_ct(): + image_sets = image_sets_normalization(ct_series) + assert len(image_sets) == 1 + sorted_files = pick_files(image_sets[0]) + assert_equal(sorted_files, ct_series) + + +def test_mr(): + assert mr_series[0].exists() + out_of_order = [ + mr_series[1], + mr_series[2], + mr_series[0], + mr_series[3], + mr_series[4], + ] + image_sets = image_sets_normalization(out_of_order) + assert image_sets + sorted_files = pick_files(image_sets[0]) + assert_equal(sorted_files, mr_series) + + +def test_series_group_by_option(): + assert mr_series[0].exists() + group_by_tags = {"tags": ["0008|0018"]} # SOP Instance UID + image_sets = image_sets_normalization(mr_series, series_group_by=group_by_tags) + assert len(image_sets) == len(mr_series) + + +def test_two_series(): + files = [ + orientation_series[1], + mr_series[4], + mr_series[2], + mr_series[1], + orientation_series[2], + orientation_series[0], + mr_series[3], + mr_series[0], + ] + assert files[0].exists() + image_sets = image_sets_normalization(files) + assert len(image_sets) == 2 + for image_set, paths in zip(image_sets, [mr_series, orientation_series]): + sorted_files = pick_files(image_set) + assert_equal(sorted_files, paths) + + +def test_three_series(): + files = [ + ct_series[0], + orientation_series[1], + mr_series[4], + mr_series[2], + mr_series[1], + orientation_series[2], + ct_series[1], + orientation_series[0], + mr_series[3], + mr_series[0], + ] + assert files[0].exists() + image_sets = image_sets_normalization(files) + assert len(image_sets) == 3 + for image_set, paths in zip(image_sets, [mr_series, orientation_series, ct_series]): + sorted_files = pick_files(image_set) + assert_equal(sorted_files, paths) diff --git a/packages/dicom/python/itkwasm-dicom/itkwasm_dicom/image_sets_normalization.py b/packages/dicom/python/itkwasm-dicom/itkwasm_dicom/image_sets_normalization.py new file mode 100644 index 000000000..0a09670b3 --- /dev/null +++ b/packages/dicom/python/itkwasm-dicom/itkwasm_dicom/image_sets_normalization.py @@ -0,0 +1,32 @@ +# Generated file. Do not edit. + +import os +from typing import Dict, Tuple, Optional, List, Any + +from itkwasm import ( + environment_dispatch, + BinaryFile, +) + +def image_sets_normalization( + files: List[os.PathLike] = [], + series_group_by: Optional[Any] = None, + image_set_group_by: Optional[Any] = None, +) -> Any: + """Group DICOM files into image sets + + :param files: DICOM files + :type files: os.PathLike + + :param series_group_by: Create series so that all instances in a series share these tags. Option is a JSON object with a "tags" array. Example tag: "0008|103e". If not provided, defaults to Series UID and Frame Of Reference UID tags. + :type series_group_by: Any + + :param image_set_group_by: Create image sets so that all series in a set share these tags. Option is a JSON object with a "tags" array. Example tag: "0008|103e". If not provided, defaults to Study UID tag. + :type image_set_group_by: Any + + :return: Image sets JSON + :rtype: Any + """ + func = environment_dispatch("itkwasm_dicom", "image_sets_normalization") + output = func(files=files, series_group_by=series_group_by, image_set_group_by=image_set_group_by) + return output diff --git a/packages/dicom/python/itkwasm-dicom/itkwasm_dicom/image_sets_normalization_async.py b/packages/dicom/python/itkwasm-dicom/itkwasm_dicom/image_sets_normalization_async.py new file mode 100644 index 000000000..acf1546b1 --- /dev/null +++ b/packages/dicom/python/itkwasm-dicom/itkwasm_dicom/image_sets_normalization_async.py @@ -0,0 +1,32 @@ +# Generated file. Do not edit. + +import os +from typing import Dict, Tuple, Optional, List, Any + +from itkwasm import ( + environment_dispatch, + BinaryFile, +) + +async def image_sets_normalization_async( + files: List[os.PathLike] = [], + series_group_by: Optional[Any] = None, + image_set_group_by: Optional[Any] = None, +) -> Any: + """Group DICOM files into image sets + + :param files: DICOM files + :type files: os.PathLike + + :param series_group_by: Create series so that all instances in a series share these tags. Option is a JSON object with a "tags" array. Example tag: "0008|103e". If not provided, defaults to Series UID and Frame Of Reference UID tags. + :type series_group_by: Any + + :param image_set_group_by: Create image sets so that all series in a set share these tags. Option is a JSON object with a "tags" array. Example tag: "0008|103e". If not provided, defaults to Study UID tag. + :type image_set_group_by: Any + + :return: Image sets JSON + :rtype: Any + """ + func = environment_dispatch("itkwasm_dicom", "image_sets_normalization_async") + output = await func(files=files, series_group_by=series_group_by, image_set_group_by=image_set_group_by) + return output diff --git a/packages/dicom/typescript/cypress/e2e/image-sets-normalization.cy.ts b/packages/dicom/typescript/cypress/e2e/image-sets-normalization.cy.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/dicom/typescript/src/image-sets-normalization-node-options.ts b/packages/dicom/typescript/src/image-sets-normalization-node-options.ts new file mode 100644 index 000000000..9e87b517d --- /dev/null +++ b/packages/dicom/typescript/src/image-sets-normalization-node-options.ts @@ -0,0 +1,17 @@ +// Generated file. To retain edits, remove this comment. + +import { BinaryFile,JsonCompatible } from 'itk-wasm' + +interface ImageSetsNormalizationNodeOptions { + /** DICOM files */ + files: string[] | File[] | BinaryFile[] + + /** Create series so that all instances in a series share these tags. Option is a JSON object with a "tags" array. Example tag: "0008|103e". If not provided, defaults to Series UID and Frame Of Reference UID tags. */ + seriesGroupBy?: JsonCompatible + + /** Create image sets so that all series in a set share these tags. Option is a JSON object with a "tags" array. Example tag: "0008|103e". If not provided, defaults to Study UID tag. */ + imageSetGroupBy?: JsonCompatible + +} + +export default ImageSetsNormalizationNodeOptions diff --git a/packages/dicom/typescript/src/image-sets-normalization-node-result.ts b/packages/dicom/typescript/src/image-sets-normalization-node-result.ts new file mode 100644 index 000000000..4d7bd9ade --- /dev/null +++ b/packages/dicom/typescript/src/image-sets-normalization-node-result.ts @@ -0,0 +1,11 @@ +// Generated file. To retain edits, remove this comment. + +import { JsonCompatible } from 'itk-wasm' + +interface ImageSetsNormalizationNodeResult { + /** Image sets JSON */ + imageSets: JsonCompatible + +} + +export default ImageSetsNormalizationNodeResult diff --git a/packages/dicom/typescript/src/image-sets-normalization-node.ts b/packages/dicom/typescript/src/image-sets-normalization-node.ts new file mode 100644 index 000000000..079a318a2 --- /dev/null +++ b/packages/dicom/typescript/src/image-sets-normalization-node.ts @@ -0,0 +1,86 @@ +// Generated file. To retain edits, remove this comment. + +import { + JsonCompatible, + InterfaceTypes, + PipelineOutput, + PipelineInput, + runPipelineNode +} from 'itk-wasm' + +import ImageSetsNormalizationNodeOptions from './image-sets-normalization-node-options.js' +import ImageSetsNormalizationNodeResult from './image-sets-normalization-node-result.js' + +import path from 'path' +import { fileURLToPath } from 'url' + +/** + * Group DICOM files into image sets + * + * @param {ImageSetsNormalizationNodeOptions} options - options object + * + * @returns {Promise} - result object + */ +async function imageSetsNormalizationNode( + options: ImageSetsNormalizationNodeOptions = { files: [] as string[], } +) : Promise { + + const mountDirs: Set = new Set() + + const desiredOutputs: Array = [ + { type: InterfaceTypes.JsonCompatible }, + ] + + const inputs: Array = [ + ] + + const args = [] + // Inputs + // Outputs + const imageSetsName = '0' + args.push(imageSetsName) + + // Options + args.push('--memory-io') + if (options.files) { + if(options.files.length < 1) { + throw new Error('"files" option must have a length > 1') + } + args.push('--files') + + options.files.forEach((value) => { + mountDirs.add(path.dirname(value as string)) + args.push(value as string) + }) + } + if (options.seriesGroupBy) { + const inputCountString = inputs.length.toString() + inputs.push({ type: InterfaceTypes.JsonCompatible, data: options.seriesGroupBy as JsonCompatible }) + args.push('--series-group-by', inputCountString) + + } + if (options.imageSetGroupBy) { + const inputCountString = inputs.length.toString() + inputs.push({ type: InterfaceTypes.JsonCompatible, data: options.imageSetGroupBy as JsonCompatible }) + args.push('--image-set-group-by', inputCountString) + + } + + const pipelinePath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'pipelines', 'image-sets-normalization') + + const { + returnValue, + stderr, + outputs + } = await runPipelineNode(pipelinePath, args, desiredOutputs, inputs, mountDirs) + if (returnValue !== 0 && stderr !== "") { + throw new Error(stderr) + } + + const result = { + imageSets: outputs[0]?.data as JsonCompatible, + } + return result +} + +export default imageSetsNormalizationNode diff --git a/packages/dicom/typescript/src/image-sets-normalization-options.ts b/packages/dicom/typescript/src/image-sets-normalization-options.ts new file mode 100644 index 000000000..11a6422d0 --- /dev/null +++ b/packages/dicom/typescript/src/image-sets-normalization-options.ts @@ -0,0 +1,17 @@ +// Generated file. To retain edits, remove this comment. + +import { BinaryFile,JsonCompatible, WorkerPoolFunctionOption } from 'itk-wasm' + +interface ImageSetsNormalizationOptions extends WorkerPoolFunctionOption { + /** DICOM files */ + files: string[] | File[] | BinaryFile[] + + /** Create series so that all instances in a series share these tags. Option is a JSON object with a "tags" array. Example tag: "0008|103e". If not provided, defaults to Series UID and Frame Of Reference UID tags. */ + seriesGroupBy?: JsonCompatible + + /** Create image sets so that all series in a set share these tags. Option is a JSON object with a "tags" array. Example tag: "0008|103e". If not provided, defaults to Study UID tag. */ + imageSetGroupBy?: JsonCompatible + +} + +export default ImageSetsNormalizationOptions diff --git a/packages/dicom/typescript/src/image-sets-normalization-result.ts b/packages/dicom/typescript/src/image-sets-normalization-result.ts new file mode 100644 index 000000000..e0a008bdd --- /dev/null +++ b/packages/dicom/typescript/src/image-sets-normalization-result.ts @@ -0,0 +1,11 @@ +// Generated file. To retain edits, remove this comment. + +import { JsonCompatible, WorkerPoolFunctionResult } from 'itk-wasm' + +interface ImageSetsNormalizationResult extends WorkerPoolFunctionResult { + /** Image sets JSON */ + imageSets: JsonCompatible + +} + +export default ImageSetsNormalizationResult diff --git a/packages/dicom/typescript/src/image-sets-normalization.ts b/packages/dicom/typescript/src/image-sets-normalization.ts new file mode 100644 index 000000000..2f9d6f7c2 --- /dev/null +++ b/packages/dicom/typescript/src/image-sets-normalization.ts @@ -0,0 +1,99 @@ +// Generated file. To retain edits, remove this comment. + +import { + JsonCompatible, + BinaryFile, + InterfaceTypes, + PipelineOutput, + PipelineInput, + runPipeline +} from 'itk-wasm' + +import ImageSetsNormalizationOptions from './image-sets-normalization-options.js' +import ImageSetsNormalizationResult from './image-sets-normalization-result.js' + +import { getPipelinesBaseUrl } from './pipelines-base-url.js' +import { getPipelineWorkerUrl } from './pipeline-worker-url.js' + +import { getDefaultWebWorker } from './default-web-worker.js' + +/** + * Group DICOM files into image sets + * + * @param {ImageSetsNormalizationOptions} options - options object + * + * @returns {Promise} - result object + */ +async function imageSetsNormalization( + options: ImageSetsNormalizationOptions = { files: [] as BinaryFile[] | File[] | string[], } +) : Promise { + + const desiredOutputs: Array = [ + { type: InterfaceTypes.JsonCompatible }, + ] + + const inputs: Array = [ + ] + + const args = [] + // Inputs + // Outputs + const imageSetsName = '0' + args.push(imageSetsName) + + // Options + args.push('--memory-io') + if (options.files) { + if(options.files.length < 1) { + throw new Error('"files" option must have a length > 1') + } + args.push('--files') + + await Promise.all(options.files.map(async (value) => { + let valueFile = value + if (value instanceof File) { + const valueBuffer = await value.arrayBuffer() + valueFile = { path: value.name, data: new Uint8Array(valueBuffer) } + } + inputs.push({ type: InterfaceTypes.BinaryFile, data: valueFile as BinaryFile }) + const name = value instanceof File ? value.name : (valueFile as BinaryFile).path + args.push(name) + })) + } + if (options.seriesGroupBy) { + const inputCountString = inputs.length.toString() + inputs.push({ type: InterfaceTypes.JsonCompatible, data: options.seriesGroupBy as JsonCompatible }) + args.push('--series-group-by', inputCountString) + + } + if (options.imageSetGroupBy) { + const inputCountString = inputs.length.toString() + inputs.push({ type: InterfaceTypes.JsonCompatible, data: options.imageSetGroupBy as JsonCompatible }) + args.push('--image-set-group-by', inputCountString) + + } + + const pipelinePath = 'image-sets-normalization' + + let workerToUse = options?.webWorker + if (workerToUse === undefined) { + workerToUse = await getDefaultWebWorker() + } + const { + webWorker: usedWebWorker, + returnValue, + stderr, + outputs + } = await runPipeline(pipelinePath, args, desiredOutputs, inputs, { pipelineBaseUrl: getPipelinesBaseUrl(), pipelineWorkerUrl: getPipelineWorkerUrl(), webWorker: workerToUse, noCopy: options?.noCopy }) + if (returnValue !== 0 && stderr !== "") { + throw new Error(stderr) + } + + const result = { + webWorker: usedWebWorker as Worker, + imageSets: outputs[0]?.data as JsonCompatible, + } + return result +} + +export default imageSetsNormalization diff --git a/packages/dicom/typescript/src/index-node-only.ts b/packages/dicom/typescript/src/index-node-only.ts index 60e7cecf0..2a0fbd87d 100644 --- a/packages/dicom/typescript/src/index-node-only.ts +++ b/packages/dicom/typescript/src/index-node-only.ts @@ -88,6 +88,15 @@ export type { WriteSegmentationNodeOptions } import writeSegmentationNode from './write-segmentation-node.js' export { writeSegmentationNode } +import ImageSetsNormalizationNodeResult from './image-sets-normalization-node-result.js' +export type { ImageSetsNormalizationNodeResult } + +import ImageSetsNormalizationNodeOptions from './image-sets-normalization-node-options.js' +export type { ImageSetsNormalizationNodeOptions } + +import imageSetsNormalizationNode from './image-sets-normalization-node.js' +export { imageSetsNormalizationNode } + import ReadDicomTagsNodeResult from './read-dicom-tags-node-result.js' export type { ReadDicomTagsNodeResult } diff --git a/packages/dicom/typescript/src/index-only.ts b/packages/dicom/typescript/src/index-only.ts index 906ffe3ec..2c1ea2cc3 100644 --- a/packages/dicom/typescript/src/index-only.ts +++ b/packages/dicom/typescript/src/index-only.ts @@ -68,7 +68,6 @@ import readImageDicomFileSeriesWorkerFunction from './read-image-dicom-file-seri export { readImageDicomFileSeriesWorkerFunction } - import ReadSegmentationResult from './read-segmentation-result.js' export type { ReadSegmentationResult } @@ -117,4 +116,14 @@ import WriteMultiSegmentationOptions from './write-multi-segmentation-options.js export type { WriteMultiSegmentationOptions } import writeMultiSegmentation from './write-multi-segmentation.js' -export { writeMultiSegmentation } \ No newline at end of file +export { writeMultiSegmentation } + + +import ImageSetsNormalizationResult from './image-sets-normalization-result.js' +export type { ImageSetsNormalizationResult } + +import ImageSetsNormalizationOptions from './image-sets-normalization-options.js' +export type { ImageSetsNormalizationOptions } + +import imageSetsNormalization from './image-sets-normalization.js' +export { imageSetsNormalization } diff --git a/packages/dicom/typescript/test/browser/demo-app/index.html b/packages/dicom/typescript/test/browser/demo-app/index.html index 0a1988215..631841b4c 100644 --- a/packages/dicom/typescript/test/browser/demo-app/index.html +++ b/packages/dicom/typescript/test/browser/demo-app/index.html @@ -39,6 +39,7 @@

👨‍💻 Live API Demo ✨

writeMultiSegmentation writeOverlappingSegmentation writeSegmentation + imageSetsNormalization @@ -477,6 +478,36 @@

👨‍💻 Live API Demo ✨

+ + + Group DICOM files into image sets

+ +
+ + +

+ + +

+ + +

+ +
Load sample inputs + Run

+ +
+ + +
+ + Download +

+
+ +
+ + Write DICOM segmentation object using multiple input images.

diff --git a/packages/dicom/typescript/test/browser/demo-app/index.ts b/packages/dicom/typescript/test/browser/demo-app/index.ts index b680fed6e..1b3954170 100644 --- a/packages/dicom/typescript/test/browser/demo-app/index.ts +++ b/packages/dicom/typescript/test/browser/demo-app/index.ts @@ -36,3 +36,5 @@ import "./structured-report-to-text-controller.js"; import "./write-multi-segmentation-controller.js"; import "./write-overlapping-segmentation-controller.js"; import "./write-segmentation-controller.js"; +import './image-sets-normalization-controller.js' +import './read-dicom-tags-controller.js' diff --git a/packages/dicom/typescript/test/node/gdcm.js b/packages/dicom/typescript/test/node/gdcm.js index bb1771f7b..2693a9b50 100644 --- a/packages/dicom/typescript/test/node/gdcm.js +++ b/packages/dicom/typescript/test/node/gdcm.js @@ -4,7 +4,7 @@ import glob from 'glob' import fs from 'fs-extra' import { IntTypes, PixelTypes, getMatrixElement } from 'itk-wasm' -import { readImageDicomFileSeriesNode, readDicomTagsNode } from '../../dist/index-node.js' +import { readImageDicomFileSeriesNode, readDicomTagsNode, imageSetsNormalizationNode } from '../../dist/index-node.js' const testDataInputDirectory = path.resolve('..', 'test', 'data', 'input') const testSeriesDirectory = path.resolve(testDataInputDirectory, 'DicomImageOrientationTest') @@ -335,3 +335,10 @@ test('DICOM SOP: Nuclear Medicine Image.', async t => { ])) t.deepEqual(outputImage.size, [128, 128, 69]) }) + +test("imageSetsNormalizationNode returns image sets", async (t) => { + const { imageSetsMetadata } = await imageSetsNormalizationNode({ + files: testDicomSeriesFiles, + }); + t.assert(!!imageSetsMetadata); +});