Class: DICOM::Anonymizer

Inherits:
Object
  • Object
show all
Includes:
Logging
Defined in:
lib/dicom/anonymizer.rb

Overview

Note:

For a thorough introduction to the concept of DICOM anonymization, please refer to The DICOM Standard, Part 15: Security and System Management Profiles, Annex E: Attribute Confidentiality Profiles. For guidance on settings for individual data elements, please refer to DICOM PS 3.15, Annex E, Table E.1-1: Application Level Confidentiality Profile Attributes.

This is a convenience class for handling the anonymization (de-identification) of DICOM files.

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Logging

included, #logger

Constructor Details

#initialize(options = {}) ⇒ Anonymizer

Note:

To customize logging behaviour, refer to the Logging module documentation.

Creates an Anonymizer instance.

Examples:

Create an Anonymizer instance and increase the log output

a = Anonymizer.new
a.logger.level = Logger::INFO

Perform anonymization using the audit trail feature

a = Anonymizer.new(:audit_trail => 'trail.json')
a.enumeration = true
a.write_path = '//anonymized/'
a.anonymize('//dicom/today/')

Parameters:

  • options (Hash) (defaults to: {})

    the options to create an anonymizer instance with

Options Hash (options):

  • :audit_trail (String)

    a file name path (if the file contains old audit data, these are loaded and used in the current anonymization)

  • :blank (Boolean)

    toggles whether to set the values of anonymized elements as empty instead of some generic value

  • :delete_private (Boolean)

    toggles whether private elements are to be deleted

  • :encryption (TrueClass, Digest::Class)

    if set as true, the default hash function (MD5) will be used for representing DICOM values in an audit file. Otherwise a Digest class can be given, e.g. Digest::SHA256

  • :enumeration (Boolean)

    toggles whether (some) elements get enumerated values (to enable post-anonymization re-identification)

  • :logger_level (Integer)

    the logger level which is applied to DObject operations during anonymization (defaults to Logger::FATAL)

  • :random_file_name (Boolean)

    toggles whether anonymized files will be given random file names when rewritten (in combination with the :write_path option)

  • :recursive (Boolean)

    toggles whether to anonymize on all sub-levels of the DICOM object tag hierarchies

  • :uid (Boolean)

    toggles whether UIDs will be replaced with custom generated UIDs (beware that to preserve UID relations in studies/series, the audit_trail feature must be used)

  • :uid_root (String)

    an organization (or custom) UID root to use when replacing UIDs

  • :write_path (String)

    a directory where the anonymized files are re-written (if not specified, files are overwritten)



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/dicom/anonymizer.rb', line 68

def initialize(options={})
  # Transfer options to attributes:
  @blank = options[:blank]
  @delete_private = options[:delete_private]
  @enumeration = options[:enumeration]
  @logger_level = options[:logger_level] || Logger::FATAL
  @random_file_name = options[:random_file_name]
  @recursive = options[:recursive]
  @uid = options[:uid]
  @uid_root = options[:uid_root] ? options[:uid_root] : UID_ROOT
  @write_path = options[:write_path]
  # Array of folders to be processed for anonymization:
  @folders = Array.new
  # Folders that will be skipped:
  @exceptions = Array.new
  # Data elements which will be anonymized (the array will hold a list of tag strings):
  @tags = Array.new
  # Default values to use on anonymized data elements:
  @values = Array.new
  # Which data elements will have enumeration applied, if requested by the user:
  @enumerations = Array.new
  # We use a Hash to store information from DICOM files if enumeration is desired:
  @enum_old_hash = Hash.new
  @enum_new_hash = Hash.new
  # All the files to be anonymized will be put in this array:
  @files = Array.new
  @prefixes = Hash.new
  # Setup audit trail if requested:
  if options[:audit_trail]
    @audit_trail_file = options[:audit_trail]
    if File.exists?(@audit_trail_file) && File.size(@audit_trail_file) > 2
      # Load the pre-existing audit trail from file:
      @audit_trail = AuditTrail.read(@audit_trail_file)
    else
      # Start from scratch with an empty audit trail:
      @audit_trail = AuditTrail.new
    end
    # Set up encryption if indicated:
    if options[:encryption]
      require 'digest'
      if options[:encryption].respond_to?(:hexdigest)
        @encryption = options[:encryption]
      else
        @encryption = Digest::MD5
      end
    end
  end
  # Set the default data elements to be anonymized:
  set_defaults
end

Instance Attribute Details

#audit_trailObject (readonly)

An AuditTrail instance used for this anonymization (if specified).



18
19
20
# File 'lib/dicom/anonymizer.rb', line 18

def audit_trail
  @audit_trail
end

#audit_trail_fileObject (readonly)

The file name used for the AuditTrail serialization (if specified).



20
21
22
# File 'lib/dicom/anonymizer.rb', line 20

def audit_trail_file
  @audit_trail_file
end

#blankObject

A boolean that if set as true will cause all anonymized tags to be blank instead of get some generic value.



22
23
24
# File 'lib/dicom/anonymizer.rb', line 22

def blank
  @blank
end

#deleteObject (readonly)

An hash of elements (represented by tag keys) that will be deleted from the DICOM objects on anonymization.



24
25
26
# File 'lib/dicom/anonymizer.rb', line 24

def delete
  @delete
end

#delete_privateObject

A boolean that if set as true, will make the anonymization delete all private tags.



26
27
28
# File 'lib/dicom/anonymizer.rb', line 26

def delete_private
  @delete_private
end

#encryptionObject (readonly)

The cryptographic hash function to be used for encrypting DICOM values recorded in an audit trail file.



28
29
30
# File 'lib/dicom/anonymizer.rb', line 28

def encryption
  @encryption
end

#enumerationObject

A boolean that if set as true will cause all anonymized tags to be get enumerated values, to enable post-anonymization re-identification by the user.



30
31
32
# File 'lib/dicom/anonymizer.rb', line 30

def enumeration
  @enumeration
end

#logger_levelObject (readonly)

The logger level which is applied to DObject operations during anonymization (defaults to Logger::FATAL).



32
33
34
# File 'lib/dicom/anonymizer.rb', line 32

def logger_level
  @logger_level
end

#random_file_nameObject

A boolean that if set as true will cause all anonymized files to be written with random file names (if write_path has been specified).



34
35
36
# File 'lib/dicom/anonymizer.rb', line 34

def random_file_name
  @random_file_name
end

#recursiveObject

A boolean that if set as true, will cause the anonymization to run on all levels of the DICOM file tag hierarchy.



36
37
38
# File 'lib/dicom/anonymizer.rb', line 36

def recursive
  @recursive
end

#uidObject

A boolean indicating whether or not UIDs shall be replaced when executing the anonymization.



38
39
40
# File 'lib/dicom/anonymizer.rb', line 38

def uid
  @uid
end

#uid_rootObject

The DICOM UID root to use when generating new UIDs.



40
41
42
# File 'lib/dicom/anonymizer.rb', line 40

def uid_root
  @uid_root
end

#write_pathObject

The path where the anonymized files will be saved. If this value is not set, the original DICOM files will be overwritten.



42
43
44
# File 'lib/dicom/anonymizer.rb', line 42

def write_path
  @write_path
end

Instance Method Details

#==(other) ⇒ Boolean Also known as: eql?

Checks for equality.

Other and self are considered equivalent if they are of compatible types and their attributes are equivalent.

Parameters:

  • other

    an object to be compared with self.

Returns:

  • (Boolean)

    true if self and other are considered equivalent



127
128
129
130
131
# File 'lib/dicom/anonymizer.rb', line 127

def ==(other)
  if other.respond_to?(:to_anonymizer)
    other.send(:state) == state
  end
end

#anonymize(dicom) ⇒ Array<DObject>

Anonymizes the given DObject or array of DICOM objects with the settings of this Anonymizer instance.

Parameters:

Returns:

  • (Array<DObject>)

    an array of the anonymized DICOM objects



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/dicom/anonymizer.rb', line 141

def anonymize(dicom)
  dicom = Array[dicom] unless dicom.respond_to?(:to_ary)
  if @tags.length > 0
    prepare_anonymization
    dicom.each do |dcm|
      anonymize_dcm(dcm.to_dcm)
    end
  else
    logger.warn("No tags have been selected for anonymization. Aborting anonymization.")
  end
  # Save the audit trail (if used):
  @audit_trail.write(@audit_trail_file) if @audit_trail
  logger.info("Anonymization complete.")
  dicom
end

#anonymize_path(path) ⇒ Object

Anonymizes any DICOM files found at the given path (file or directory) with the settings of this Anonymizer instance.

Parameters:

  • path (String)

    a file or directory path



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/dicom/anonymizer.rb', line 162

def anonymize_path(path)
  if @tags.length > 0
    prepare_anonymization
    files = DICOM.load_files(path)
    logger.info("#{files.length} DICOM files have been prepared for anonymization.")
    files.each do |f|
      dcm = anonymize_file(f)
      write(dcm)
    end
  else
    logger.warn("No tags have been selected for anonymization. Aborting anonymization.")
  end
  # Save the audit trail (if used):
  @audit_trail.write(@audit_trail_file) if @audit_trail
  logger.info("Anonymization complete.")
end

#delete_tag(tag) ⇒ Object

Specifies that the given tag is to be completely deleted from the anonymized DICOM objects.

Examples:

Completely delete the Patient’s Name tag from the DICOM files

a.delete_tag('0010,0010')

Parameters:

  • tag (String)

    a data element tag

Raises:

  • (ArgumentError)


186
187
188
189
190
# File 'lib/dicom/anonymizer.rb', line 186

def delete_tag(tag)
  raise ArgumentError, "Expected String, got #{tag.class}." unless tag.is_a?(String)
  raise ArgumentError, "Expected a valid tag of format 'GGGG,EEEE', got #{tag}." unless tag.tag?
  @delete[tag] = true
end

#enum(tag) ⇒ Boolean, NilClass

Checks the enumeration status of this tag.

Parameters:

  • tag (String)

    a data element tag

Returns:

  • (Boolean, NilClass)

    the enumeration status of the tag, or nil if the tag has no match

Raises:

  • (ArgumentError)


197
198
199
200
201
202
203
204
205
206
207
# File 'lib/dicom/anonymizer.rb', line 197

def enum(tag)
  raise ArgumentError, "Expected String, got #{tag.class}." unless tag.is_a?(String)
  raise ArgumentError, "Expected a valid tag of format 'GGGG,EEEE', got #{tag}." unless tag.tag?
  pos = @tags.index(tag)
  if pos
    return @enumerations[pos]
  else
    logger.warn("The specified tag (#{tag}) was not found in the list of tags to be anonymized.")
    return nil
  end
end

#hashInteger

Note:

Two objects with the same attributes will have the same hash code.

Computes a hash code for this object.

Returns:

  • (Integer)

    the object’s hash code



215
216
217
# File 'lib/dicom/anonymizer.rb', line 215

def hash
  state.hash
end

#remove_tag(tag) ⇒ Object

Removes a tag from the list of tags that will be anonymized.

Examples:

Do not anonymize the Patient’s Name tag

a.remove_tag('0010,0010')

Parameters:

  • tag (String)

    a data element tag

Raises:

  • (ArgumentError)


225
226
227
228
229
230
231
232
233
234
# File 'lib/dicom/anonymizer.rb', line 225

def remove_tag(tag)
  raise ArgumentError, "Expected String, got #{tag.class}." unless tag.is_a?(String)
  raise ArgumentError, "Expected a valid tag of format 'GGGG,EEEE', got #{tag}." unless tag.tag?
  pos = @tags.index(tag)
  if pos
    @tags.delete_at(pos)
    @values.delete_at(pos)
    @enumerations.delete_at(pos)
  end
end

#set_tag(tag, options = {}) ⇒ Object

Sets the anonymization settings for the specified tag. If the tag is already present in the list of tags to be anonymized, its settings are updated, and if not, a new tag entry is created.

Examples:

Set the anonymization settings of the Patient’s Name tag

a.set_tag('0010,0010', :value => 'MrAnonymous', :enum => true)

Parameters:

  • tag (String)

    a data element tag

  • options (Hash) (defaults to: {})

    the anonymization settings for the specified tag

Options Hash (options):

  • :value (String, Integer, Float)

    the replacement value to be used when anonymizing this data element. Defaults to the pre-existing value and ” for new tags.

  • :enum (String, Integer, Float)

    specifies if enumeration is to be used for this tag. Defaults to the pre-existing value and false for new tags.

Raises:

  • (ArgumentError)


246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/dicom/anonymizer.rb', line 246

def set_tag(tag, options={})
  raise ArgumentError, "Expected String, got #{tag.class}." unless tag.is_a?(String)
  raise ArgumentError, "Expected a valid tag of format 'GGGG,EEEE', got #{tag}." unless tag.tag?
  pos = @tags.index(tag)
  if pos
    # Update existing values:
    @values[pos] = options[:value] if options[:value]
    @enumerations[pos] = options[:enum] if options[:enum] != nil
  else
    # Add new elements:
    @tags << tag
    @values << (options[:value] ? options[:value] : default_value(tag))
    @enumerations << (options[:enum] ? options[:enum] : false)
  end
end

#to_anonymizerAnonymizer

Returns self.

Returns:



266
267
268
# File 'lib/dicom/anonymizer.rb', line 266

def to_anonymizer
  self
end

#value(tag) ⇒ String, ...

Note:

If enumeration is selected for a string type tag, a number will be appended in addition to the string that is returned here.

Gives the value which will be used when anonymizing this tag.

Parameters:

  • tag (String)

    a data element tag

Returns:

  • (String, Integer, Float, NilClass)

    the replacement value for the specified tag, or nil if the tag is not matched

Raises:

  • (ArgumentError)


278
279
280
281
282
283
284
285
286
287
288
# File 'lib/dicom/anonymizer.rb', line 278

def value(tag)
  raise ArgumentError, "Expected String, got #{tag.class}." unless tag.is_a?(String)
  raise ArgumentError, "Expected a valid tag of format 'GGGG,EEEE', got #{tag}." unless tag.tag?
  pos = @tags.index(tag)
  if pos
    return @values[pos]
  else
    logger.warn("The specified tag (#{tag}) was not found in the list of tags to be anonymized.")
    return nil
  end
end