# HG changeset patch # User Alain Mazy # Date 1742830455 -3600 # Node ID 7902d830c9a517549363a5f1d1d1661c48bbbf0d # Parent 0a60d2d482d4d1bd740316833468b511d02c787b pixel masking: handling of multiframe images diff -r 0a60d2d482d4 -r 7902d830c9a5 OrthancFramework/Sources/DicomFormat/DicomTag.h --- a/OrthancFramework/Sources/DicomFormat/DicomTag.h Tue Mar 18 10:08:43 2025 +0100 +++ b/OrthancFramework/Sources/DicomFormat/DicomTag.h Mon Mar 24 16:34:15 2025 +0100 @@ -102,6 +102,8 @@ static const DicomTag DICOM_TAG_SERIES_DESCRIPTION(0x0008, 0x103e); static const DicomTag DICOM_TAG_MODALITY(0x0008, 0x0060); + static const DicomTag DICOM_TAG_DETECTOR_INFORMATION_SEQUENCE(0x0054, 0x0022); + // The following is used for "modify/anonymize" operations static const DicomTag DICOM_TAG_SOP_CLASS_UID(0x0008, 0x0016); static const DicomTag DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID(0x0002, 0x0002); @@ -194,6 +196,7 @@ static const DicomTag DICOM_TAG_RESCALE_INTERCEPT(0x0028, 0x1052); static const DicomTag DICOM_TAG_RESCALE_SLOPE(0x0028, 0x1053); static const DicomTag DICOM_TAG_SLICE_THICKNESS(0x0018, 0x0050); + static const DicomTag DICOM_TAG_SPACING_BETWEEN_SLICES(0x0018, 0x0088); static const DicomTag DICOM_TAG_WINDOW_CENTER(0x0028, 0x1050); static const DicomTag DICOM_TAG_WINDOW_WIDTH(0x0028, 0x1051); static const DicomTag DICOM_TAG_DOSE_GRID_SCALING(0x3004, 0x000e); diff -r 0a60d2d482d4 -r 7902d830c9a5 OrthancFramework/Sources/DicomParsing/DicomPixelMasker.cpp --- a/OrthancFramework/Sources/DicomParsing/DicomPixelMasker.cpp Tue Mar 18 10:08:43 2025 +0100 +++ b/OrthancFramework/Sources/DicomParsing/DicomPixelMasker.cpp Mon Mar 24 16:34:15 2025 +0100 @@ -116,18 +116,12 @@ return false; } - static void GetDoubleVector(std::vector& target, const ParsedDicomFile& file, const DicomTag& tag, size_t expectedSize) + static void GetDoubleVector(std::vector& target, const std::string& strValue, const DicomTag& tag, size_t expectedSize) { target.clear(); - std::string str; - if (!file.GetTagValue(str, tag)) - { - throw OrthancException(ErrorCode_InexistentTag, "Unable to perform 3D -> 2D conversion, missing tag" + tag.Format()); - } - std::vector strVector; - Toolbox::SplitString(strVector, str, '\\'); + Toolbox::SplitString(strVector, strValue, '\\'); if (strVector.size() != expectedSize) { @@ -147,32 +141,88 @@ } } + static void GetDoubleVector(std::vector& target, const ParsedDicomFile& file, const DicomTag& tag, size_t expectedSize) + { + std::string str; + if (!file.GetTagValue(str, tag)) + { + throw OrthancException(ErrorCode_InexistentTag, "Unable to perform 3D -> 2D conversion, missing tag" + tag.Format()); + } + + GetDoubleVector(target, str, tag, expectedSize); + } + bool DicomPixelMasker::Region3D::GetPixelMaskArea(unsigned int& x1, unsigned int& y1, unsigned int& x2, unsigned int& y2, const ParsedDicomFile& file, unsigned int frameIndex) const { if (IsTargeted(file)) { + DicomMap tags; + file.ExtractDicomSummary(tags, 256); + std::vector imagePositionPatient; std::vector imageOrientationPatient; std::vector pixelSpacing; + double sliceSpacing = 0.0; - GetDoubleVector(imagePositionPatient, file, DICOM_TAG_IMAGE_POSITION_PATIENT, 3); - GetDoubleVector(imageOrientationPatient, file, DICOM_TAG_IMAGE_ORIENTATION_PATIENT, 6); + if (file.HasTag(DICOM_TAG_IMAGE_POSITION_PATIENT) && file.HasTag(DICOM_TAG_IMAGE_ORIENTATION_PATIENT)) + { + GetDoubleVector(imagePositionPatient, file, DICOM_TAG_IMAGE_POSITION_PATIENT, 3); + GetDoubleVector(imageOrientationPatient, file, DICOM_TAG_IMAGE_ORIENTATION_PATIENT, 6); + } + else if (file.HasTag(DICOM_TAG_DETECTOR_INFORMATION_SEQUENCE)) // find it in the detector info sequence (for some multi frame instances like NM or scintigraphy) TODO-PIXEL-ANON: to validate + { + const Json::Value& jsonSequence = tags.GetValue(DICOM_TAG_DETECTOR_INFORMATION_SEQUENCE).GetSequenceContent(); + if (jsonSequence.size() == 1) + { + std::string strImagePositionPatient = jsonSequence[0]["0020,0032"]["Value"].asString(); + std::string strImageOrientationPatient = jsonSequence[0]["0020,0037"]["Value"].asString(); + + GetDoubleVector(imagePositionPatient, strImagePositionPatient, DICOM_TAG_IMAGE_POSITION_PATIENT, 3); + GetDoubleVector(imageOrientationPatient, strImageOrientationPatient, DICOM_TAG_IMAGE_ORIENTATION_PATIENT, 6); + } + else + { + throw OrthancException(ErrorCode_InternalError, "Unable to find ImagePositionPatient in DetectorInformationSequence, invalid sequence size"); + } + } + else + { + throw OrthancException(ErrorCode_InternalError, "Unable to find ImagePositionPatient or ImageOrientationPatient"); + } + GetDoubleVector(pixelSpacing, file, DICOM_TAG_PIXEL_SPACING, 2); + if (file.HasTag(DICOM_TAG_SPACING_BETWEEN_SLICES)) + { + std::string strSliceSpacing; + if (file.GetTagValue(strSliceSpacing, DICOM_TAG_SPACING_BETWEEN_SLICES)) + { + sliceSpacing = boost::lexical_cast(strSliceSpacing); + } + } + + double z = imagePositionPatient[2]; + + if (sliceSpacing != 0.0) + { + z = z - frameIndex * sliceSpacing; + } + // note: To simplify, for the z, we only check that imagePositionPatient is between the authorized z values. // This won't be perfectly true for weird images with slices that are not parallel but let's wait for someone to complain ... - if (imagePositionPatient[2] < std::min(z1_, z2_) || - imagePositionPatient[2] > std::max(z1_, z2_)) + if (z < std::min(z1_, z2_) || + z > std::max(z1_, z2_)) { return false; } + double deltaX1 = x1_ - imagePositionPatient[0]; double deltaY1 = y1_ - imagePositionPatient[1]; - double deltaZ1 = z1_ - imagePositionPatient[2]; + double deltaZ1 = z1_ - z; double deltaX2 = x2_ - imagePositionPatient[0]; double deltaY2 = y2_ - imagePositionPatient[1]; - double deltaZ2 = z2_ - imagePositionPatient[2]; + double deltaZ2 = z2_ - z; double ix1 = (deltaX1 * imageOrientationPatient[0] + deltaY1 * imageOrientationPatient[1] + deltaZ1 * imageOrientationPatient[2]) / pixelSpacing[0]; double iy1 = (deltaX1 * imageOrientationPatient[3] + deltaY1 * imageOrientationPatient[4] + deltaZ1 * imageOrientationPatient[5]) / pixelSpacing[1]; diff -r 0a60d2d482d4 -r 7902d830c9a5 TODO --- a/TODO Tue Mar 18 10:08:43 2025 +0100 +++ b/TODO Mon Mar 24 16:34:15 2025 +0100 @@ -570,4 +570,43 @@ }] } } -EOF \ No newline at end of file +EOF + + +# modify some frames of a multiframe scintigraphy study +curl -X POST http://localhost:8043/studies/ab67d5f8-95865506-8fb83c8b-93610651-ddce6e77/modify \ +--data-binary @- << EOF +{ + "Replace" : {"StudyInstanceUID": "1.2.6", "StudyDescription": "modified-scinti"}, + "Force": true, + "MaskPixelData": { + "Regions": [{ + "MaskType": "Fill", + "FillValue": 3000, + "RegionType" : "3D", + "Origin": [-150.0, -200, 200], + "End": [150.0, -150, 100] + }] + } +} +EOF + + +# modify some frames of a multiframe PET-CT study +curl -X POST http://localhost:8043/studies/890e1167-55ad171a-7721ffec-db91e2c1-700778c0/modify \ +--data-binary @- << EOF +{ + "Replace" : {"StudyInstanceUID": "1.2.7", "StudyDescription": "modified-pet-ct"}, + "Force": true, + "MaskPixelData": { + "Regions": [{ + "MaskType": "Fill", + "FillValue": 3000, + "RegionType" : "3D", + "Origin": [-150.0, -100, -85], + "End": [150.0, 100, -250] + }] + } +} +EOF +