view OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp @ 2751:0dfc804b3e5d Orthanc-0.7.4

close old branch
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 17 Jul 2018 09:39:45 +0200
parents 5197fd35333c
children aebf0071020e
line wrap: on
line source

/**
 * 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);
  }
}