# HG changeset patch # User Sebastien Jodogne # Date 1399035991 -7200 # Node ID efd0215736d9cd8329423c29bb988c1a8ee973c1 # Parent a60040857ce65a24cd3c46446ef2a9fc13960730 start of anonymization refactoring diff -r a60040857ce6 -r efd0215736d9 OrthancServer/FromDcmtkBridge.cpp --- a/OrthancServer/FromDcmtkBridge.cpp Fri May 02 12:59:05 2014 +0200 +++ b/OrthancServer/FromDcmtkBridge.cpp Fri May 02 15:06:31 2014 +0200 @@ -1732,4 +1732,24 @@ return false; } } + + + void ParsedDicomFile::SaveToFile(const std::string& path) + { + // TODO Avoid using a temporary memory buffer, write directly on disk + std::string content; + SaveToMemoryBuffer(content); + Toolbox::WriteFile(content, path); + } + + + ParsedDicomFile::ParsedDicomFile() + { + file_.reset(new DcmFileFormat); + Replace(DICOM_TAG_PATIENT_ID, FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Patient)); + Replace(DICOM_TAG_STUDY_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Study)); + Replace(DICOM_TAG_SERIES_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Series)); + Replace(DICOM_TAG_SOP_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Instance)); + } + } diff -r a60040857ce6 -r efd0215736d9 OrthancServer/FromDcmtkBridge.h --- a/OrthancServer/FromDcmtkBridge.h Fri May 02 12:59:05 2014 +0200 +++ b/OrthancServer/FromDcmtkBridge.h Fri May 02 15:06:31 2014 +0200 @@ -58,66 +58,6 @@ DicomReplaceMode_IgnoreIfAbsent }; - class ParsedDicomFile : public IDynamicObject - { - private: - std::auto_ptr file_; - - ParsedDicomFile(DcmFileFormat& other) : - file_(dynamic_cast(other.clone())) - { - } - - void Setup(const char* content, - size_t size); - - public: - ParsedDicomFile(const char* content, - size_t size) - { - Setup(content, size); - } - - ParsedDicomFile(const std::string& content) - { - if (content.size() == 0) - Setup(NULL, 0); - else - Setup(&content[0], content.size()); - } - - DcmFileFormat& GetDicom() - { - return *file_; - } - - ParsedDicomFile* Clone() - { - return new ParsedDicomFile(*file_); - } - - void SendPathValue(RestApiOutput& output, - const UriComponents& uri); - - void Answer(RestApiOutput& output); - - void Remove(const DicomTag& tag); - - void Insert(const DicomTag& tag, - const std::string& value); - - void Replace(const DicomTag& tag, - const std::string& value, - DicomReplaceMode mode); - - void RemovePrivateTags(); - - bool GetTagValue(std::string& value, - const DicomTag& tag); - - DicomInstanceHasher GetHasher(); - }; - class FromDcmtkBridge { public: @@ -184,4 +124,74 @@ static bool SaveToMemoryBuffer(std::string& buffer, DcmDataset* dataSet); }; + + class ParsedDicomFile : public IDynamicObject + { + private: + std::auto_ptr file_; + + ParsedDicomFile(DcmFileFormat& other) : + file_(dynamic_cast(other.clone())) + { + } + + void Setup(const char* content, + size_t size); + + public: + ParsedDicomFile(); // Create a minimal DICOM instance + + ParsedDicomFile(const char* content, + size_t size) + { + Setup(content, size); + } + + ParsedDicomFile(const std::string& content) + { + if (content.size() == 0) + Setup(NULL, 0); + else + Setup(&content[0], content.size()); + } + + DcmFileFormat& GetDicom() + { + return *file_; + } + + ParsedDicomFile* Clone() + { + return new ParsedDicomFile(*file_); + } + + void SendPathValue(RestApiOutput& output, + const UriComponents& uri); + + void Answer(RestApiOutput& output); + + void Remove(const DicomTag& tag); + + void Insert(const DicomTag& tag, + const std::string& value); + + void Replace(const DicomTag& tag, + const std::string& value, + DicomReplaceMode mode = DicomReplaceMode_InsertIfAbsent); + + void RemovePrivateTags(); + + bool GetTagValue(std::string& value, + const DicomTag& tag); + + DicomInstanceHasher GetHasher(); + + void SaveToMemoryBuffer(std::string& buffer) + { + FromDcmtkBridge::SaveToMemoryBuffer(buffer, file_->getDataset()); + } + + void SaveToFile(const std::string& path); + }; + } diff -r a60040857ce6 -r efd0215736d9 UnitTestsSources/FromDcmtk.cpp --- a/UnitTestsSources/FromDcmtk.cpp Fri May 02 12:59:05 2014 +0200 +++ b/UnitTestsSources/FromDcmtk.cpp Fri May 02 15:06:31 2014 +0200 @@ -2,6 +2,7 @@ #include "../OrthancServer/FromDcmtkBridge.h" #include "../OrthancServer/OrthancInitialization.h" +#include "../Core/OrthancException.h" using namespace Orthanc; @@ -23,3 +24,319 @@ } +namespace Orthanc +{ + class DicomModification + { + /** + * Process: + * (1) Remove private tags + * (2) Remove tags specified by the user + * (3) Replace tags + **/ + + private: + typedef std::set Removals; + typedef std::map Replacements; + typedef std::map< std::pair, std::string> UidMap; + + Removals removals_; + Replacements replacements_; + bool removePrivateTags_; + DicomRootLevel level_; + UidMap uidMap_; + + void MapDicomIdentifier(ParsedDicomFile& dicom, + DicomRootLevel level) + { + std::auto_ptr tag; + + switch (level) + { + case DicomRootLevel_Study: + tag.reset(new DicomTag(DICOM_TAG_STUDY_INSTANCE_UID)); + break; + + case DicomRootLevel_Series: + tag.reset(new DicomTag(DICOM_TAG_SERIES_INSTANCE_UID)); + break; + + case DicomRootLevel_Instance: + tag.reset(new DicomTag(DICOM_TAG_SOP_INSTANCE_UID)); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + std::string original; + if (!dicom.GetTagValue(original, *tag)) + { + original = ""; + } + + std::string mapped; + //bool isNew; + + UidMap::const_iterator previous = uidMap_.find(std::make_pair(level, original)); + if (previous == uidMap_.end()) + { + mapped = FromDcmtkBridge::GenerateUniqueIdentifier(level); + uidMap_.insert(std::make_pair(std::make_pair(level, original), mapped)); + //isNew = true; + } + else + { + mapped = previous->second; + //isNew = false; + } + + dicom.Replace(*tag, mapped); + + //return isNew; + } + + public: + DicomModification() + { + removePrivateTags_ = false; + level_ = DicomRootLevel_Instance; + } + + void Keep(const DicomTag& tag) + { + removals_.erase(tag); + } + + void Remove(const DicomTag& tag) + { + removals_.insert(tag); + replacements_.erase(tag); + } + + bool IsRemoved(const DicomTag& tag) const + { + return removals_.find(tag) != removals_.end(); + } + + void Replace(const DicomTag& tag, + const std::string& value) + { + removals_.erase(tag); + replacements_[tag] = value; + } + + bool IsReplaced(const DicomTag& tag) const + { + return replacements_.find(tag) != replacements_.end(); + } + + const std::string& GetReplacement(const DicomTag& tag) const + { + Replacements::const_iterator it = replacements_.find(tag); + + if (it == replacements_.end()) + { + throw OrthancException(ErrorCode_InexistentItem); + } + else + { + return it->second; + } + } + + void SetRemovePrivateTags(bool removed) + { + removePrivateTags_ = removed; + } + + bool ArePrivateTagsRemoved() const + { + return removePrivateTags_; + } + + void SetLevel(DicomRootLevel level) + { + uidMap_.clear(); + level_ = level; + } + + DicomRootLevel GetLevel() const + { + return level_; + } + + void SetupAnonymization() + { + removals_.clear(); + replacements_.clear(); + removePrivateTags_ = true; + level_ = DicomRootLevel_Patient; + uidMap_.clear(); + + // This is Table E.1-1 from PS 3.15-2008 - DICOM Part 15: Security and System Management Profiles + removals_.insert(DicomTag(0x0008, 0x0014)); // Instance Creator UID + //removals_.insert(DicomTag(0x0008, 0x0018)); // SOP Instance UID => set in Apply() + removals_.insert(DicomTag(0x0008, 0x0050)); // Accession Number + removals_.insert(DicomTag(0x0008, 0x0080)); // Institution Name + removals_.insert(DicomTag(0x0008, 0x0081)); // Institution Address + removals_.insert(DicomTag(0x0008, 0x0090)); // Referring Physician's Name + removals_.insert(DicomTag(0x0008, 0x0092)); // Referring Physician's Address + removals_.insert(DicomTag(0x0008, 0x0094)); // Referring Physician's Telephone Numbers + removals_.insert(DicomTag(0x0008, 0x1010)); // Station Name + removals_.insert(DicomTag(0x0008, 0x1030)); // Study Description + removals_.insert(DicomTag(0x0008, 0x103e)); // Series Description + removals_.insert(DicomTag(0x0008, 0x1040)); // Institutional Department Name + removals_.insert(DicomTag(0x0008, 0x1048)); // Physician(s) of Record + removals_.insert(DicomTag(0x0008, 0x1050)); // Performing Physicians' Name + removals_.insert(DicomTag(0x0008, 0x1060)); // Name of Physician(s) Reading Study + removals_.insert(DicomTag(0x0008, 0x1070)); // Operators' Name + removals_.insert(DicomTag(0x0008, 0x1080)); // Admitting Diagnoses Description + removals_.insert(DicomTag(0x0008, 0x1155)); // Referenced SOP Instance UID + removals_.insert(DicomTag(0x0008, 0x2111)); // Derivation Description + //removals_.insert(DicomTag(0x0010, 0x0010)); // Patient's Name => cf. below (*) + //removals_.insert(DicomTag(0x0010, 0x0020)); // Patient ID => cf. below (*) + removals_.insert(DicomTag(0x0010, 0x0030)); // Patient's Birth Date + removals_.insert(DicomTag(0x0010, 0x0032)); // Patient's Birth Time + removals_.insert(DicomTag(0x0010, 0x0040)); // Patient's Sex + removals_.insert(DicomTag(0x0010, 0x1000)); // Other Patient Ids + removals_.insert(DicomTag(0x0010, 0x1001)); // Other Patient Names + removals_.insert(DicomTag(0x0010, 0x1010)); // Patient's Age + removals_.insert(DicomTag(0x0010, 0x1020)); // Patient's Size + removals_.insert(DicomTag(0x0010, 0x1030)); // Patient's Weight + removals_.insert(DicomTag(0x0010, 0x1090)); // Medical Record Locator + removals_.insert(DicomTag(0x0010, 0x2160)); // Ethnic Group + removals_.insert(DicomTag(0x0010, 0x2180)); // Occupation + removals_.insert(DicomTag(0x0010, 0x21b0)); // Additional Patient's History + removals_.insert(DicomTag(0x0010, 0x4000)); // Patient Comments + removals_.insert(DicomTag(0x0018, 0x1000)); // Device Serial Number + removals_.insert(DicomTag(0x0018, 0x1030)); // Protocol Name + //removals_.insert(DicomTag(0x0020, 0x000d)); // Study Instance UID => set in Apply() + //removals_.insert(DicomTag(0x0020, 0x000e)); // Series Instance UID => set in Apply() + removals_.insert(DicomTag(0x0020, 0x0010)); // Study ID + removals_.insert(DicomTag(0x0020, 0x0052)); // Frame of Reference UID + removals_.insert(DicomTag(0x0020, 0x0200)); // Synchronization Frame of Reference UID + removals_.insert(DicomTag(0x0020, 0x4000)); // Image Comments + removals_.insert(DicomTag(0x0040, 0x0275)); // Request Attributes Sequence + removals_.insert(DicomTag(0x0040, 0xa124)); // UID + removals_.insert(DicomTag(0x0040, 0xa730)); // Content Sequence + removals_.insert(DicomTag(0x0088, 0x0140)); // Storage Media File-set UID + removals_.insert(DicomTag(0x3006, 0x0024)); // Referenced Frame of Reference UID + removals_.insert(DicomTag(0x3006, 0x00c2)); // Related Frame of Reference UID + + // Some more removals (from the experience of DICOM files at the CHU of Liege) + removals_.insert(DicomTag(0x0010, 0x1040)); // Patient's Address + removals_.insert(DicomTag(0x0032, 0x1032)); // Requesting Physician + removals_.insert(DicomTag(0x0010, 0x2154)); // PatientTelephoneNumbers + removals_.insert(DicomTag(0x0010, 0x2000)); // Medical Alerts + + // Set the DeidentificationMethod tag + replacements_.insert(std::make_pair(DicomTag(0x0012, 0x0063), "Orthanc " ORTHANC_VERSION " - PS 3.15-2008 Table E.1-1")); + + // Set the PatientIdentityRemoved tag + replacements_.insert(std::make_pair(DicomTag(0x0012, 0x0062), "YES")); + + // (*) Choose a random patient name and ID + std::string patientId = FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Patient); + replacements_[DICOM_TAG_PATIENT_ID] = patientId; + replacements_[DICOM_TAG_PATIENT_NAME] = patientId; + } + + void Apply(ParsedDicomFile& toModify) + { + // Check the request + assert(DicomRootLevel_Patient + 1 == DicomRootLevel_Study && + DicomRootLevel_Study + 1 == DicomRootLevel_Series && + DicomRootLevel_Series + 1 == DicomRootLevel_Instance); + + if (IsRemoved(DICOM_TAG_PATIENT_ID) || + IsRemoved(DICOM_TAG_STUDY_INSTANCE_UID) || + IsRemoved(DICOM_TAG_SERIES_INSTANCE_UID) || + IsRemoved(DICOM_TAG_SOP_INSTANCE_UID)) + { + throw OrthancException(ErrorCode_BadRequest); + } + + if (level_ == DicomRootLevel_Patient && !IsReplaced(DICOM_TAG_PATIENT_ID)) + { + throw OrthancException(ErrorCode_BadRequest); + } + + if (level_ > DicomRootLevel_Patient && IsReplaced(DICOM_TAG_PATIENT_ID)) + { + throw OrthancException(ErrorCode_BadRequest); + } + + if (level_ > DicomRootLevel_Study && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) + { + throw OrthancException(ErrorCode_BadRequest); + } + + if (level_ > DicomRootLevel_Series && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID)) + { + throw OrthancException(ErrorCode_BadRequest); + } + + // (1) Remove the private tags, if need be + if (removePrivateTags_) + { + toModify.RemovePrivateTags(); + } + + // (2) Remove the tags specified by the user + for (Removals::const_iterator it = removals_.begin(); + it != removals_.end(); ++it) + { + toModify.Remove(*it); + } + + // (3) Replace the tags + for (Replacements::const_iterator it = replacements_.begin(); + it != replacements_.end(); ++it) + { + toModify.Replace(it->first, it->second, DicomReplaceMode_InsertIfAbsent); + } + + // Update the DICOM identifiers + if (level_ <= DicomRootLevel_Study) + { + MapDicomIdentifier(toModify, DicomRootLevel_Study); + } + + if (level_ <= DicomRootLevel_Series) + { + MapDicomIdentifier(toModify, DicomRootLevel_Series); + } + + if (level_ <= DicomRootLevel_Instance) // Always true + { + MapDicomIdentifier(toModify, DicomRootLevel_Instance); + } + } + }; +} + + + +TEST(DicomModification, Basic) +{ + DicomModification m; + m.SetupAnonymization(); + //m.SetLevel(DicomRootLevel_Study); + //m.Replace(DICOM_TAG_PATIENT_ID, "coucou"); + //m.Replace(DICOM_TAG_PATIENT_NAME, "coucou"); + + ParsedDicomFile o; + o.SaveToFile("/tmp/tutu.dcm"); + + for (int i = 0; i < 10; i++) + { + char b[1024]; + sprintf(b, "/tmp/tutu%06d.dcm", i); + std::auto_ptr f(o.Clone()); + if (i > 4) + o.Replace(DICOM_TAG_SERIES_INSTANCE_UID, "coucou"); + m.Apply(*f); + f->SaveToFile(b); + } +}