Mercurial > hg > orthanc
diff OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp @ 751:5197fd35333c
refactoring of OrthancRestApi
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Mon, 14 Apr 2014 11:28:35 +0200 |
parents | |
children | aebf0071020e |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Mon Apr 14 11:28:35 2014 +0200 @@ -0,0 +1,692 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "OrthancRestApi.h" + +#include <glog/logging.h> + +namespace Orthanc +{ + // TODO IMPROVE MULTITHREADING + // Every call to "ParsedDicomFile" must lock this mutex!!! + static boost::mutex cacheMutex_; + + + // Raw access to the DICOM tags of an instance ------------------------------ + + static void GetRawContent(RestApi::GetCall& call) + { + boost::mutex::scoped_lock lock(cacheMutex_); + + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string id = call.GetUriComponent("id", ""); + ParsedDicomFile& dicom = context.GetDicomFile(id); + dicom.SendPathValue(call.GetOutput(), call.GetTrailingUri()); + } + + + + // Modification of DICOM instances ------------------------------------------ + + namespace + { + typedef std::set<DicomTag> Removals; + typedef std::map<DicomTag, std::string> Replacements; + typedef std::map< std::pair<DicomRootLevel, std::string>, std::string> UidMap; + } + + static void ReplaceInstanceInternal(ParsedDicomFile& toModify, + const Removals& removals, + const Replacements& replacements, + DicomReplaceMode mode, + bool removePrivateTags) + { + if (removePrivateTags) + { + toModify.RemovePrivateTags(); + } + + for (Removals::const_iterator it = removals.begin(); + it != removals.end(); ++it) + { + toModify.Remove(*it); + } + + for (Replacements::const_iterator it = replacements.begin(); + it != replacements.end(); ++it) + { + toModify.Replace(it->first, it->second, mode); + } + + // A new SOP instance UID is automatically generated + std::string instanceUid = FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Instance); + toModify.Replace(DICOM_TAG_SOP_INSTANCE_UID, instanceUid, DicomReplaceMode_InsertIfAbsent); + } + + + static void ParseRemovals(Removals& target, + const Json::Value& removals) + { + if (!removals.isArray()) + { + throw OrthancException(ErrorCode_BadRequest); + } + + for (Json::Value::ArrayIndex i = 0; i < removals.size(); i++) + { + std::string name = removals[i].asString(); + DicomTag tag = FromDcmtkBridge::ParseTag(name); + target.insert(tag); + + VLOG(1) << "Removal: " << name << " " << tag << std::endl; + } + } + + + static void ParseReplacements(Replacements& target, + const Json::Value& replacements) + { + if (!replacements.isObject()) + { + throw OrthancException(ErrorCode_BadRequest); + } + + Json::Value::Members members = replacements.getMemberNames(); + for (size_t i = 0; i < members.size(); i++) + { + const std::string& name = members[i]; + std::string value = replacements[name].asString(); + + DicomTag tag = FromDcmtkBridge::ParseTag(name); + target[tag] = value; + + VLOG(1) << "Replacement: " << name << " " << tag << " == " << value << std::endl; + } + } + + + static std::string GeneratePatientName(ServerContext& context) + { + uint64_t seq = context.GetIndex().IncrementGlobalSequence(GlobalProperty_AnonymizationSequence); + return "Anonymized" + boost::lexical_cast<std::string>(seq); + } + + + static void SetupAnonymization(Removals& removals, + Replacements& replacements) + { + // 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 by ReplaceInstanceInternal() + 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 + //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 => cf. below (*) + //removals.insert(DicomTag(0x0020, 0x000e)); // Series Instance UID => cf. below (*) + 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 + + /** + * (*) Patient ID, Study Instance UID and Series Instance UID + * are modified by "AnonymizeInstance()" if anonymizing a single + * instance, or by "RetrieveMappedUid()" if anonymizing a + * patient/study/series. + **/ + + + // 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")); + } + + + static bool ParseModifyRequest(Removals& removals, + Replacements& replacements, + bool& removePrivateTags, + const RestApi::PostCall& call) + { + removePrivateTags = false; + Json::Value request; + if (call.ParseJsonRequest(request) && + request.isObject()) + { + Json::Value removalsPart = Json::arrayValue; + Json::Value replacementsPart = Json::objectValue; + + if (request.isMember("Remove")) + { + removalsPart = request["Remove"]; + } + + if (request.isMember("Replace")) + { + replacementsPart = request["Replace"]; + } + + if (request.isMember("RemovePrivateTags")) + { + removePrivateTags = true; + } + + ParseRemovals(removals, removalsPart); + ParseReplacements(replacements, replacementsPart); + + return true; + } + else + { + return false; + } + } + + + static bool ParseAnonymizationRequest(Removals& removals, + Replacements& replacements, + bool& removePrivateTags, + bool& keepPatientId, + RestApi::PostCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + removePrivateTags = true; + keepPatientId = false; + + Json::Value request; + if (call.ParseJsonRequest(request) && + request.isObject()) + { + Json::Value keepPart = Json::arrayValue; + Json::Value removalsPart = Json::arrayValue; + Json::Value replacementsPart = Json::objectValue; + + if (request.isMember("Keep")) + { + keepPart = request["Keep"]; + } + + if (request.isMember("KeepPrivateTags")) + { + removePrivateTags = false; + } + + if (request.isMember("Replace")) + { + replacementsPart = request["Replace"]; + } + + Removals toKeep; + ParseRemovals(toKeep, keepPart); + + SetupAnonymization(removals, replacements); + + for (Removals::iterator it = toKeep.begin(); it != toKeep.end(); ++it) + { + if (*it == DICOM_TAG_PATIENT_ID) + { + keepPatientId = true; + } + + removals.erase(*it); + } + + Removals additionalRemovals; + ParseRemovals(additionalRemovals, removalsPart); + + for (Removals::iterator it = additionalRemovals.begin(); + it != additionalRemovals.end(); ++it) + { + removals.insert(*it); + } + + ParseReplacements(replacements, replacementsPart); + + // Generate random Patient's Name if none is specified + if (toKeep.find(DICOM_TAG_PATIENT_NAME) == toKeep.end() && + replacements.find(DICOM_TAG_PATIENT_NAME) == replacements.end()) + { + replacements.insert(std::make_pair(DICOM_TAG_PATIENT_NAME, GeneratePatientName(context))); + } + + return true; + } + else + { + return false; + } + } + + + static void AnonymizeOrModifyInstance(Removals& removals, + Replacements& replacements, + bool removePrivateTags, + RestApi::PostCall& call) + { + boost::mutex::scoped_lock lock(cacheMutex_); + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string id = call.GetUriComponent("id", ""); + ParsedDicomFile& dicom = context.GetDicomFile(id); + + std::auto_ptr<ParsedDicomFile> modified(dicom.Clone()); + ReplaceInstanceInternal(*modified, removals, replacements, DicomReplaceMode_InsertIfAbsent, removePrivateTags); + modified->Answer(call.GetOutput()); + } + + + static bool RetrieveMappedUid(ParsedDicomFile& dicom, + DicomRootLevel level, + Replacements& replacements, + UidMap& uidMap) + { + std::auto_ptr<DicomTag> tag; + + switch (level) + { + case DicomRootLevel_Series: + tag.reset(new DicomTag(DICOM_TAG_SERIES_INSTANCE_UID)); + break; + + case DicomRootLevel_Study: + tag.reset(new DicomTag(DICOM_TAG_STUDY_INSTANCE_UID)); + break; + + case DicomRootLevel_Patient: + tag.reset(new DicomTag(DICOM_TAG_PATIENT_ID)); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + std::string original; + if (!dicom.GetTagValue(original, *tag)) + { + throw OrthancException(ErrorCode_InternalError); + } + + 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; + } + + replacements[*tag] = mapped; + return isNew; + } + + + static void AnonymizeOrModifyResource(Removals& removals, + Replacements& replacements, + bool removePrivateTags, + bool keepPatientId, + MetadataType metadataType, + ChangeType changeType, + ResourceType resourceType, + RestApi::PostCall& call) + { + typedef std::list<std::string> Instances; + + bool isFirst = true; + Json::Value result(Json::objectValue); + + boost::mutex::scoped_lock lock(cacheMutex_); + ServerContext& context = OrthancRestApi::GetContext(call); + + Instances instances; + std::string id = call.GetUriComponent("id", ""); + context.GetIndex().GetChildInstances(instances, id); + + if (instances.empty()) + { + return; + } + + /** + * Loop over all the instances of the resource. + **/ + + UidMap uidMap; + for (Instances::const_iterator it = instances.begin(); + it != instances.end(); ++it) + { + LOG(INFO) << "Modifying instance " << *it; + ParsedDicomFile& original = context.GetDicomFile(*it); + + DicomInstanceHasher originalHasher = original.GetHasher(); + + if (isFirst && keepPatientId) + { + std::string patientId = originalHasher.GetPatientId(); + uidMap[std::make_pair(DicomRootLevel_Patient, patientId)] = patientId; + } + + bool isNewSeries = RetrieveMappedUid(original, DicomRootLevel_Series, replacements, uidMap); + bool isNewStudy = RetrieveMappedUid(original, DicomRootLevel_Study, replacements, uidMap); + bool isNewPatient = RetrieveMappedUid(original, DicomRootLevel_Patient, replacements, uidMap); + + + /** + * Compute the resulting DICOM instance and store it into the Orthanc store. + **/ + + std::auto_ptr<ParsedDicomFile> modified(original.Clone()); + ReplaceInstanceInternal(*modified, removals, replacements, DicomReplaceMode_InsertIfAbsent, removePrivateTags); + + std::string modifiedInstance; + if (context.Store(modifiedInstance, modified->GetDicom()) != StoreStatus_Success) + { + LOG(ERROR) << "Error while storing a modified instance " << *it; + return; + } + + + /** + * Record metadata information (AnonymizedFrom/ModifiedFrom). + **/ + + DicomInstanceHasher modifiedHasher = modified->GetHasher(); + + if (isNewSeries) + { + context.GetIndex().SetMetadata(modifiedHasher.HashSeries(), + metadataType, originalHasher.HashSeries()); + } + + if (isNewStudy) + { + context.GetIndex().SetMetadata(modifiedHasher.HashStudy(), + metadataType, originalHasher.HashStudy()); + } + + if (isNewPatient) + { + context.GetIndex().SetMetadata(modifiedHasher.HashPatient(), + metadataType, originalHasher.HashPatient()); + } + + assert(*it == originalHasher.HashInstance()); + assert(modifiedInstance == modifiedHasher.HashInstance()); + context.GetIndex().SetMetadata(modifiedInstance, metadataType, *it); + + + /** + * Compute the JSON object that is returned by the REST call. + **/ + + if (isFirst) + { + std::string newId; + + switch (resourceType) + { + case ResourceType_Series: + newId = modifiedHasher.HashSeries(); + break; + + case ResourceType_Study: + newId = modifiedHasher.HashStudy(); + break; + + case ResourceType_Patient: + newId = modifiedHasher.HashPatient(); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + result["Type"] = EnumerationToString(resourceType); + result["ID"] = newId; + result["Path"] = GetBasePath(resourceType, newId); + result["PatientID"] = modifiedHasher.HashPatient(); + isFirst = false; + } + } + + call.GetOutput().AnswerJson(result); + } + + + + static void ModifyInstance(RestApi::PostCall& call) + { + Removals removals; + Replacements replacements; + bool removePrivateTags; + + if (ParseModifyRequest(removals, replacements, removePrivateTags, call)) + { + AnonymizeOrModifyInstance(removals, replacements, removePrivateTags, call); + } + } + + + static void AnonymizeInstance(RestApi::PostCall& call) + { + Removals removals; + Replacements replacements; + bool removePrivateTags, keepPatientId; + + if (ParseAnonymizationRequest(removals, replacements, removePrivateTags, keepPatientId, call)) + { + // TODO Handle "keepPatientId" + + // Generate random patient ID if not specified + if (replacements.find(DICOM_TAG_PATIENT_ID) == replacements.end()) + { + replacements.insert(std::make_pair(DICOM_TAG_PATIENT_ID, + FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Patient))); + } + + // Generate random study UID if not specified + if (replacements.find(DICOM_TAG_STUDY_INSTANCE_UID) == replacements.end()) + { + replacements.insert(std::make_pair(DICOM_TAG_STUDY_INSTANCE_UID, + FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Study))); + } + + // Generate random series UID if not specified + if (replacements.find(DICOM_TAG_SERIES_INSTANCE_UID) == replacements.end()) + { + replacements.insert(std::make_pair(DICOM_TAG_SERIES_INSTANCE_UID, + FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Series))); + } + + AnonymizeOrModifyInstance(removals, replacements, removePrivateTags, call); + } + } + + + static void ModifySeriesInplace(RestApi::PostCall& call) + { + Removals removals; + Replacements replacements; + bool removePrivateTags; + + if (ParseModifyRequest(removals, replacements, removePrivateTags, call)) + { + AnonymizeOrModifyResource(removals, replacements, removePrivateTags, true /*keepPatientId*/, + MetadataType_ModifiedFrom, ChangeType_ModifiedSeries, + ResourceType_Series, call); + } + } + + + static void AnonymizeSeriesInplace(RestApi::PostCall& call) + { + Removals removals; + Replacements replacements; + bool removePrivateTags, keepPatientId; + + if (ParseAnonymizationRequest(removals, replacements, removePrivateTags, keepPatientId, call)) + { + AnonymizeOrModifyResource(removals, replacements, removePrivateTags, keepPatientId, + MetadataType_AnonymizedFrom, ChangeType_AnonymizedSeries, + ResourceType_Series, call); + } + } + + + static void ModifyStudyInplace(RestApi::PostCall& call) + { + Removals removals; + Replacements replacements; + bool removePrivateTags; + + if (ParseModifyRequest(removals, replacements, removePrivateTags, call)) + { + AnonymizeOrModifyResource(removals, replacements, removePrivateTags, true /*keepPatientId*/, + MetadataType_ModifiedFrom, ChangeType_ModifiedStudy, + ResourceType_Study, call); + } + } + + + static void AnonymizeStudyInplace(RestApi::PostCall& call) + { + Removals removals; + Replacements replacements; + bool removePrivateTags, keepPatientId; + + if (ParseAnonymizationRequest(removals, replacements, removePrivateTags, keepPatientId, call)) + { + AnonymizeOrModifyResource(removals, replacements, removePrivateTags, keepPatientId, + MetadataType_AnonymizedFrom, ChangeType_AnonymizedStudy, + ResourceType_Study, call); + } + } + + + /*static void ModifyPatientInplace(RestApi::PostCall& call) + { + Removals removals; + Replacements replacements; + bool removePrivateTags; + + if (ParseModifyRequest(removals, replacements, removePrivateTags, call)) + { + AnonymizeOrModifyResource(false, removals, replacements, removePrivateTags, + MetadataType_ModifiedFrom, ChangeType_ModifiedPatient, + ResourceType_Patient, call); + } + }*/ + + + static void AnonymizePatientInplace(RestApi::PostCall& call) + { + Removals removals; + Replacements replacements; + bool removePrivateTags, keepPatientId; + + if (ParseAnonymizationRequest(removals, replacements, removePrivateTags, keepPatientId, call)) + { + AnonymizeOrModifyResource(removals, replacements, removePrivateTags, keepPatientId, + MetadataType_AnonymizedFrom, ChangeType_AnonymizedPatient, + ResourceType_Patient, call); + } + } + + + + void OrthancRestApi::RegisterAnonymizeModify() + { + Register("/instances/{id}/content/*", GetRawContent); + + Register("/instances/{id}/modify", ModifyInstance); + Register("/series/{id}/modify", ModifySeriesInplace); + Register("/studies/{id}/modify", ModifyStudyInplace); + //Register("/patients/{id}/modify", ModifyPatientInplace); + + Register("/instances/{id}/anonymize", AnonymizeInstance); + Register("/series/{id}/anonymize", AnonymizeSeriesInplace); + Register("/studies/{id}/anonymize", AnonymizeStudyInplace); + Register("/patients/{id}/anonymize", AnonymizePatientInplace); + } +}