dicom 0.3 → 0.4

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,27 @@
1
+ =0.4
2
+
3
+ === 3rd February, 2009
4
+
5
+ * Change of syntax: Keywords are now supplied as hash.
6
+ * Method below() renamed to children().
7
+ * Added method parents() which returns the position of all items/sequences that are the parent of
8
+ the specified tag. This method is only relevant for DICOM tags that exist inside a hierarchy.
9
+ * Simplified the code in class Dictionary: load time is ~ 40 times faster!
10
+ * Improved the code in class DRead: average read time is reduced by ~ 15 %.
11
+ * Improved support for Big Endian DICOM files.
12
+ * Added support for the "FL" (floating point single) value representation.
13
+ * Fixed a small bug with tag 0028,3006 in Dictionary.
14
+ * New method image_to_file() which makes it even easier than before to dump the DICOM pixel data to file.
15
+ * Introducing DWrite class which enables writing DICOM objects to file.
16
+ * Added several methods in DObject class to take advantage of write capability:
17
+ remove_tag()
18
+ set_value()
19
+ set_image_magick()
20
+ set_image_file()
21
+ write_file()
22
+ * Introducing the Anonymizer class, which takes advantage of the new write capability in Ruby DICOM
23
+ to offer a fairly powerful and customizable tool for anonymizing your DICOM files.
24
+
1
25
  = 0.3
2
26
 
3
27
  === 12th October, 2008
@@ -10,6 +34,8 @@
10
34
  * Method print() is able to print to file as well. The text file will be put in the same folder as the DICOM file.
11
35
  * New method below(), which lets you specify a sequence or item, and this method will return the position
12
36
  of all tags contained in this sequence/item.
37
+ * Method print_properties() have been updated to display more information about the DICOM object.
38
+
13
39
 
14
40
  = 0.2
15
41
 
@@ -43,7 +69,7 @@ Known issues:
43
69
  * 12 bit image data not supported
44
70
  * Color images not supported in NArray and RMagick retrieve methods
45
71
  * Unpacking compressed image data has basic support but is not properly tested yet.
46
- * Reading Big Endian files probably has some issues
47
- * Reading Little Endian files on a Big Endian system probably has issues as well
48
- * Reading of multiple frame image data is not particularly robust at this time
49
- * Retrieving images when file contains two or more unrelated images is not handled correctly
72
+ * Reading Big Endian has basic, but not full, support.
73
+ * Reading on a Big Endian system is not tested.
74
+ * Reading of multiple frame image data to RMagick does not work in all cases.
75
+ * Retrieving images when file contains two or more unrelated images may not be handled correctly.
@@ -1,11 +1,14 @@
1
- DICOM is a small library for reading DICOM files. It is written completely in Ruby and has no external dependencies.
1
+ DICOM is a small library for reading, editing and writing DICOM files.
2
+ It is written completely in Ruby and has no external dependencies.
2
3
 
3
- Copyright 2008 Christoffer Lervåg (chris.lervag@gmail.com)
4
+ Copyright 2008-2009 Christoffer Lervåg (chris.lervag [@nospam] @gmail.com)
4
5
 
5
6
  INSTALLATION
6
7
 
7
8
  gem install dicom
8
9
 
10
+ *************************************************************************
11
+
9
12
  DOCUMENTATION
10
13
 
11
14
  CLASS DLibrary
@@ -19,32 +22,47 @@ PUBLIC CLASS METHODS
19
22
  because you can save time by loading the library one time at startup instead of
20
23
  having the library being loaded for each DICOM file being read.
21
24
  Example:
22
- lib = DLibrary.new()
25
+ myLib = DICOM::DLibrary.new
23
26
 
24
27
 
25
28
  CLASS DObject
26
29
 
27
30
  PUBLIC CLASS METHODS
28
31
 
29
- new(filename, *options)
32
+ new(filename, options={})
30
33
 
31
34
  Initialize a new DICOM object.
32
- Example 1 (The simple way):
35
+ Example 1: (The simplest way)
33
36
  require 'dicom'
34
37
  obj = DICOM::DObject.new("myFile.dcm")
35
- Example 2 (Using a pre-loaded library to speed up reading when reading multiple files):
36
- obj = DICOM::DObject.new("myFile.dcm", verbose=false, library=lib)
38
+ Example 2: (Using a pre-loaded library to speed up reading when reading multiple files)
39
+ obj = DICOM::DObject.new("myFile.dcm", :verbose => false, :lib => myLib)
40
+ Example 3: (Open an empty DICOM object)
41
+ obj = DICOM::DObject.new(nil)
42
+
43
+ ACCESSORS
44
+ :read_success
45
+ A boolean that is true if DICOM object was read successfully, and false if not.
46
+ :write_success
47
+ A boolean that is true if DICOM object was written successfully, and false if not.
48
+ :modality
49
+ A string which holds the description of the modality of the DICOM object that has been read.
50
+ Example of use:
51
+ obj = DICOM::DObject.new("myFile.dcm")
52
+ if obj.read_success
53
+ puts obj.modality
54
+ end
37
55
 
38
56
  PUBLIC INSTANCE METHODS
39
57
 
40
- below(id, *options)
58
+ children(id, options={})
41
59
  Returns the positions of all tags inside the hierarchy of a sequence or an item.
42
60
  This is useful if you later want to get the position(s) of a certain tag,
43
61
  restricted to the positions inside the given sequence or item.
44
62
  Example 1: (Return all tag positions that is contained in the following sequence)
45
- pos = obj.below("3006,0082")
63
+ pos = obj.children("3006,0082")
46
64
  Example 2: (Return all tag positions that is contained only directly beneath the following sequence)
47
- pos = obj.below("3006,0082", next_only=true)
65
+ pos = obj.children("3006,0082", :next_only => true)
48
66
 
49
67
  get_frames()
50
68
  Returns the number of frames present in the image data in the DICOM file.
@@ -70,12 +88,13 @@ PUBLIC INSTANCE METHODS
70
88
  get_image_pos()
71
89
  Returns the index(es) of the tag(s) that contain image data.
72
90
 
73
- get_pos(id, *options)
91
+ get_pos(id, options={})
74
92
  Returns the index(es) of the tag(s) in the DICOM file that match the supplied tag ID.
75
93
  Example 1: (Find all occurences of the specified tag in the object)
76
94
  pos = obj.get_pos("3006,0080")
77
95
  Example 2: (Find all occurences of the specified tag inside the specified sequence)
78
- pos = obj.get_pos("3006,0080", obj.below("3006,0082"))
96
+ selection = obj.children("3006,0082")
97
+ pos = obj.get_pos("3006,0080", :array => selection)
79
98
 
80
99
  get_raw(id)
81
100
  Returns the raw data of the DICOM tag that matches the supplied tag ID.
@@ -85,14 +104,24 @@ PUBLIC INSTANCE METHODS
85
104
  Returns the value (processed raw data) of the DICOM tag that matches the supplied tag ID.
86
105
  The ID may be a tag index, tag name or tag label.
87
106
 
88
- print(id, *options)
107
+ image_to_file(file)
108
+ Dumps the pixel data of the DICOM object directly to the specified file.
109
+ This is useful if you wish to extract this data to process it with another program.
110
+
111
+ parents(id)
112
+ Returns the positions of all parents of this tag in the hierarchy.
113
+ This is useful if you want to know the position of the items or sequence tags that 'hold' your tag.
114
+ Example:
115
+ pos = obj.parents("300C,0006")
116
+
117
+ print(id, options={})
89
118
  Prints the information of one or many tag(s):
90
119
  (index, [hierarchy level,] label, name, type, length, value)
91
120
  The method can print to both screen or to a text file. If print to file is chosen,
92
121
  the text file will be put in the folder of the original DICOM file with a '.txt' extension.
93
122
  The ID may be a tag name, label or position, or it might be an array of positions.
94
123
  Example 1: (Print all tags to file, with both tree visualization and level numbers)
95
- obj.print(true, levels=true, tree=true, file=true)
124
+ obj.print(true, :levels => true, :tree => true, :file => true)
96
125
  Example 2: (Print an array of tags to screen, no level or tree visualization)
97
126
  obj.print([4,5,6])
98
127
 
@@ -101,3 +130,95 @@ PUBLIC INSTANCE METHODS
101
130
 
102
131
  print_properties()
103
132
  Prints the key structural properties of the DICOM file to the screen.
133
+
134
+ remove_tag(tag)
135
+ Removes the specified tag from the DICOM object. You can use this method
136
+ if you are editing a DICOM object and wants to get rid of some tags.
137
+
138
+ set_value(value, options={})
139
+ This method can be used both to edit an existing tag, or to create new tags in your DICOM object.
140
+ Example 1: (Edit patient name)
141
+ obj.set_value("Anonymous", :label => "0010,0010", :create => false)
142
+ Example 2: (Insert binary data for a specific tag)
143
+ obj.set_value(data, :pos => 52, :bin => true, :create => false)
144
+
145
+ set_image_magick(object)
146
+ Inserts a RMagick image object to the pixel data tag of your DICOM object.
147
+
148
+ set_image_file(file)
149
+ Inserts the binary content of a file to the Pixel Data tag in your DICOM object.
150
+ This can be useful if you have processed some image data using a custom program
151
+ and just wants to put that data back into a DICOM object.
152
+
153
+ write_file(file)
154
+ Writes the DICOM object to the specified file.
155
+ Example:
156
+ obj.write_file(myPath + "test_file.dcm")
157
+
158
+
159
+ CLASS Anonymizer
160
+
161
+ PUBLIC CLASS METHODS
162
+
163
+ new()
164
+ Initialize a new Anonymizer instance.
165
+ Example:
166
+ a = DICOM::Anonymizer.new
167
+
168
+ ACCESSORS
169
+ :blank
170
+ A boolean that you can set if you want to all anonymization tags to be blank
171
+ instead of having some generic value.
172
+ :enumeration
173
+ A boolean that if set will make the script set enumerated values on anonymized tags,
174
+ such that you are able to separate the DICOM files of unique individuals after anonymization.
175
+ Example of fictious result:
176
+ "Joe Sixpack" => "Person1" and "Joe Schmoe" => "Person2"
177
+ :identity_file
178
+ If you request enumeration, you can specify an identity file which will enable you to reidentify
179
+ the anonymized DICOM files at a later stage. The relationship between original names and
180
+ enumerated values is stored in a text file which you can keep for yourself, while handing out
181
+ the anonymized DICOM files to a third party.
182
+
183
+ :write_path
184
+ You may set a different path for where the anonymized DICOM files will be stored. If this
185
+ value is not set, the Anonymizer script will overwrite the old DICOM files.
186
+ Example:
187
+ a.write_path = "C:/temp/"
188
+
189
+ PUBLIC INSTANCE METHODS
190
+
191
+ add_folder(path)
192
+ Adds a folder who's files (including all files in subfolders) will be anonymized.
193
+ Example:
194
+ a.add_folder("/home/dicom")
195
+
196
+ add_exception(path)
197
+ Adds a folder who's files (including all files in its subfolders) will be excluded from anonymization.
198
+
199
+ add_tag(tag, options={})
200
+ Adds a tag to the list of tags that will be anonymized. As options you can specify value to be used
201
+ and whether the tag should be included for enumeration if this feature has been activated.
202
+ Example:
203
+ a.add_tag("0010,0010, :value => "MrAnonymous", :enum => true)
204
+
205
+ change_enum(tag, status)
206
+ Sets enumeration status for a specific tag. Status = true means the selected tag will get
207
+ an enumerated value, false means it will not.
208
+
209
+ execute(verbose)
210
+ Executes the anonymization process. Run this method when you are finished choosing all your settings.
211
+ Verbose (=true/false) will apply to the read/update/write process that takes place in DObject, and not
212
+ the messages of the Anonymization script itself.
213
+
214
+ print()
215
+ Prints the list of tags that have been selected for anonymization, along with the values
216
+ that the original tags will be replaced with. If enumeration is selected, this method will also
217
+ print which tags have been selected for enumeration.
218
+
219
+ remove_tag(tag)
220
+ Removes a tag from the list of tags that will be anonymized.
221
+ Example:
222
+ a.remove_tag("0010,0010")
223
+
224
+
data/README CHANGED
@@ -4,7 +4,7 @@ RUBY DICOM
4
4
  SUMMARY
5
5
  --------
6
6
 
7
- This is a fairly basic library for reading DICOM files in Ruby. Digital Imaging and Communications in Medicine (DICOM) is a standard for handling, storing, printing, and transmitting information in medical imaging. It includes a file format definition and a network communications protocol. The library currently only supports reading of the file format.
7
+ This is a fairly basic library for reading DICOM files in Ruby. Digital Imaging and Communications in Medicine (DICOM) is a standard for handling, storing, printing, and transmitting information in medical imaging. It includes a file format definition and a network communications protocol. The library supports reading, editing and writing the file format.
8
8
 
9
9
  BASIC USAGE
10
10
  -----------
@@ -15,7 +15,7 @@ dcm = DICOM::DObject.new("myFile.dcm")
15
15
  # Display some key information about the file:
16
16
  dcm.print_properties()
17
17
  # Print all tags to screen:
18
- dcm.print_all()
18
+ dcm.print(true)
19
19
  # Retrieve a tag value:
20
20
  name = dcm.get_value("0010.0010")
21
21
  # Retrieve pixel data:
@@ -25,12 +25,21 @@ image = dcm.get_image_magick()
25
25
  image[0].display
26
26
  # Load pixel data in a NArray object and display on screen:
27
27
  image = dcm.get_image_narray()
28
- NImage.show image[0,true,true]
28
+ NImage.show image[0,true,true]
29
+
30
+ Tip:
31
+ When playing around with Ruby DICOM in irb, you may be annoyed
32
+ with all the information that is printed to screen, regardless
33
+ if you have specified verbose as false. This is because in irb
34
+ every variable loaded in the program is automatically printed.
35
+ A hack to avoid this effect is to append ";0" after a command.
36
+ Example:
37
+ dcm = DICOM::DObject.new("myFile.dcm") ;0
29
38
 
30
39
  COPYRIGHT
31
40
  ---------
32
41
 
33
- Copyright 2008 Christoffer Lervåg
42
+ Copyright 2008-2009 Christoffer Lervåg
34
43
 
35
44
  This program is free software: you can redistribute it and/or modify
36
45
  it under the terms of the GNU General Public License as published by
@@ -56,6 +65,6 @@ Location:
56
65
  Oslo, Norway
57
66
 
58
67
  Email:
59
- chris.lervag @nospam @gmail.com
68
+ chris.lervag [@nospam] @gmail.com
60
69
  Please don't hesitate to email me if have any thoughts on this project!
61
70
 
@@ -0,0 +1,433 @@
1
+ # Copyright 2008-2009 Christoffer Lerv�g
2
+ module DICOM
3
+
4
+ # Class for anonymizing DICOM files:
5
+ # A good resource on this topic (report from the DICOM standards committee, work group 18):
6
+ # ftp://medical.nema.org/medical/dicom/Supps/sup142_03.pdf
7
+ class Anonymizer
8
+
9
+ attr_accessor :blank, :enumeration, :identity_file, :verbose, :write_path
10
+
11
+ # Initialize the Anonymizer instance:
12
+ def initialize(opts={})
13
+ # Default verbosity is true: # NB: verbosity is not used currently
14
+ @verbose = opts[:verbose]
15
+ @verbose = true if @verbose == nil
16
+ # Load library:
17
+ @lib = DLibrary.new
18
+ # Default value of accessors:
19
+ @blank = false
20
+ @enumeration = false
21
+ @write_path = nil
22
+ # Array of folders to be processed for anonymization:
23
+ @folders = Array.new
24
+ @exceptions = Array.new
25
+ # Tags that will be anonymized:
26
+ @tags = Array.new
27
+ # Default values to use on anonymized tags:
28
+ @values = Array.new
29
+ # Which tags will have enumeration applied, if requested by the user:
30
+ @enum = Array.new
31
+ # We use a hash to store information from DICOM files if enumeration is desired:
32
+ @enum_old_hash = {}
33
+ @enum_new_hash = {}
34
+ # All the files to be anonymized will be put in this array:
35
+ @files = Array.new
36
+ # Write paths will be determined later and put in this array:
37
+ @write_paths = Array.new
38
+ # Set the default tags to be anonymized:
39
+ set_defaults()
40
+ end # of method initialize
41
+
42
+
43
+ # Adds a folder who's files will be anonymized:
44
+ def add_folder(path)
45
+ @folders += [path] if path
46
+ end
47
+
48
+
49
+ # Adds an exception folder that is to be avoided when anonymizing:
50
+ def add_exception(path)
51
+ @exceptions += [path] if path
52
+ end
53
+
54
+
55
+ # Adds a tag to the list of tags that will be anonymized:
56
+ def add_tag(tag, opts={})
57
+ # Options and defaults:
58
+ value = opts[:value] || ""
59
+ enum = opts[:enum] || false
60
+ if tag
61
+ if tag.is_a?(String)
62
+ if tag.length == 9
63
+ # Add tag information:
64
+ @tags += [tag]
65
+ @values += [value]
66
+ @enum += [enum]
67
+ else
68
+ puts "Warning: Invalid tag length. Please use the form 'GGGG,EEEE'."
69
+ end
70
+ else
71
+ puts "Warning: Tag is not a string. Can not add tag."
72
+ end
73
+ else
74
+ puts "Warning: No tag supplied. Nothing to add."
75
+ end
76
+ end # of method add_tag
77
+
78
+
79
+ # Set enumeration status for a specific tag (toggle true/false)
80
+ def change_enum(tag, enum)
81
+ pos = @tags.index(tag)
82
+ if pos
83
+ if enum
84
+ @enum[pos] = true
85
+ else
86
+ @enum[pos] = false
87
+ end
88
+ else
89
+ puts "Specified tag not found in anonymization array. No changes made."
90
+ end
91
+ end # of method change_enum
92
+
93
+
94
+ # Changes the value used in anonymization for a specific tag:
95
+ def change_value(tag, value)
96
+ pos = @tags.index(tag)
97
+ if pos
98
+ if value
99
+ @values[pos] = value
100
+ else
101
+ puts "No value were specified. No changes made."
102
+ end
103
+ else
104
+ puts "Specified tag not found in anonymization array. No changes made."
105
+ end
106
+ end # of method change_value
107
+
108
+
109
+ # Executes the anonymization process:
110
+ def execute(verbose=false)
111
+ # Search through the folders to gather all the files to be anonymized:
112
+ puts "*******************************************************"
113
+ puts "Initiating anonymization process."
114
+ start_time = Time.now.to_f
115
+ puts "Searching for files..."
116
+ load_files()
117
+ puts "Done."
118
+ if @files.length > 0
119
+ if @tags.length > 0
120
+ puts @files.length.to_s + " files have been identified in the specified folder(s)."
121
+ if @write_path
122
+ # Determine the write paths, as anonymized files will be written to a separate location:
123
+ puts "Processing write paths..."
124
+ process_write_paths()
125
+ puts "Done"
126
+ else
127
+ # Overwriting old files:
128
+ puts "Separate write folder not specified. Will overwrite existing DICOM files."
129
+ @write_paths = @files
130
+ end
131
+ # If the user wants enumeration, we need to prepare variables for storing
132
+ # existing information associated with each tag:
133
+ create_enum_hash() if @enumeration
134
+ # Start the read/update/write process:
135
+ puts "Initiating read/update/write process..."
136
+ # Monitor whether every file read/write was successful:
137
+ all_read = true
138
+ all_write = true
139
+ @files.each_index do |i|
140
+ # Read existing file to DICOM object:
141
+ obj = DICOM::DObject.new(@files[i], :verbose => verbose, :lib => @lib)
142
+ if obj.read_success
143
+ # Anonymize the desired tags:
144
+ @tags.each_index do |j|
145
+ if @blank
146
+ value = ""
147
+ elsif @enumeration
148
+ # Get old value:
149
+ current = obj.get_value(@tags[j])
150
+ # Only launch enumeration logic if tag exists:
151
+ if current != false
152
+ value = get_enumeration_value(current, j)
153
+ else
154
+ value = ""
155
+ end
156
+ else
157
+ # Value is simply value in array:
158
+ value = @values[j]
159
+ end # of if @blank..else..
160
+ # Update DICOM object with new value:
161
+ obj.set_value(value, :create => false, :label => @tags[j])
162
+ end
163
+ # Write DICOM file:
164
+ obj.write_file(@write_paths[i])
165
+ all_write = false unless obj.write_success
166
+ else
167
+ all_read = false
168
+ end
169
+ end # of @files.each...
170
+ end_time = Time.now.to_f
171
+ puts "Anonymization process completed!"
172
+ if all_read
173
+ puts "All files in specified folder(s) were SUCCESSFULLY read to DICOM objects."
174
+ else
175
+ puts "Some files were NOT successfully read. If folder(s) contain non-DICOM files, then this is probably the reason."
176
+ end
177
+ if all_write
178
+ puts "All DICOM objects were SUCCESSFULLY written as DICOM files."
179
+ else
180
+ puts "Some DICOM objects were NOT succesfully written to file. You are advised to have a closer look."
181
+ end
182
+ # Has user requested enumeration and specified an identity file in which to store the anonymized values?
183
+ if @enumeration and @identity_file
184
+ puts "Writing identity file."
185
+ write_identity_file()
186
+ puts "Done"
187
+ end
188
+ elapsed = (end_time-start_time).to_s
189
+ puts "Elapsed time: " + elapsed[0..elapsed.index(".")+1] + " seconds"
190
+ else
191
+ puts "No tags have been selected for anonymization. Aborting."
192
+ end
193
+ else
194
+ puts "No files were found in specified folders. Aborting."
195
+ end
196
+ puts "*******************************************************"
197
+ end # of method execute
198
+
199
+
200
+ # Prints a list of which tags are currently selected for anonymization along with
201
+ # replacement values that will be used and enumeration status.
202
+ def print()
203
+ # Extract the string lengths which are needed to make the formatting nice:
204
+ names = Array.new
205
+ types = Array.new
206
+ label_lengths = Array.new
207
+ name_lengths = Array.new
208
+ type_lengths = Array.new
209
+ value_lengths = Array.new
210
+ @tags.each_index do |i|
211
+ arr = @lib.get_name_vr(@tags[i])
212
+ names += [arr[0]]
213
+ types += [arr[1]]
214
+ label_lengths[i] = @tags[i].length
215
+ name_lengths[i] = names[i].length
216
+ type_lengths[i] = types[i].length
217
+ value_lengths[i] = @values[i].to_s.length unless @blank
218
+ value_lengths[i] = "" if @blank
219
+ end
220
+ # To give the printed output a nice format we need to check the string lengths of some of these arrays:
221
+ label_maxL = label_lengths.max
222
+ name_maxL = name_lengths.max
223
+ type_maxL = type_lengths.max
224
+ value_maxL = value_lengths.max
225
+ # Format string array for print output:
226
+ lines = Array.new
227
+ @tags.each_index do |i|
228
+ # Configure empty spaces:
229
+ s = " "
230
+ f1 = " "*(label_maxL-@tags[i].length+1)
231
+ f2 = " "*(name_maxL-names[i].length+1)
232
+ f3 = " "*(type_maxL-types[i].length+1)
233
+ f4 = " " if @blank
234
+ f4 = " "*(value_maxL-@values[i].to_s.length+1) unless @blank
235
+ if @enumeration
236
+ enum = @enum[i]
237
+ else
238
+ enum = ""
239
+ end
240
+ if @blank
241
+ value = ""
242
+ else
243
+ value = @values [i]
244
+ end
245
+ tag = @tags[i]
246
+ lines += [tag + f1 + names[i] + f2 + types[i] + f3 + value.to_s + f4 + enum.to_s ]
247
+ end
248
+ # Print to screen:
249
+ lines.each do |line|
250
+ puts line
251
+ end
252
+ end # of method print_tags
253
+
254
+
255
+ # Removes a tag from the list of tags that will be anonymized:
256
+ def remove_tag(tag)
257
+ pos = @tags.index(tag)
258
+ if pos
259
+ @tags.delete_at(pos)
260
+ @values.delete_at(pos)
261
+ @enum.delete_at(pos)
262
+ else
263
+ puts "Specified tag not found in anonymization array. No changes made."
264
+ end
265
+ end # of method remove_tag
266
+
267
+
268
+ # The following methods are private:
269
+ private
270
+
271
+
272
+ # Finds the common path in an array of files, by performing a recursive search.
273
+ # Returns the index of the last folder in str_arr that is common in all file paths.
274
+ def common_path(str_arr, index)
275
+ common_folders = Array.new
276
+ # Find out how much of the path is similar for all files in @files array:
277
+ folder = str_arr[index]
278
+ all_match = true
279
+ @files.each do |f|
280
+ all_match = false unless f.include?(folder)
281
+ end
282
+ if all_match
283
+ # Need to check the next folder in the array:
284
+ result = common_path(str_arr, index + 1)
285
+ else
286
+ # Current folder did not match, which means last possible match is current index -1.
287
+ result = index - 1
288
+ end
289
+ return result
290
+ end # of method common_path
291
+
292
+
293
+ # Creates a hash that is used for storing information used when enumeration is desired.
294
+ def create_enum_hash()
295
+ @enum.each_index do |i|
296
+ @enum_old_hash[@tags[i]] = Array.new
297
+ @enum_new_hash[@tags[i]] = Array.new
298
+ end
299
+ end
300
+
301
+
302
+ # Handles enumeration for current DICOM tag:
303
+ def get_enumeration_value(current, j)
304
+ # Is enumeration requested for this tag?
305
+ if @enum[j]
306
+ # Retrieve earlier used anonymization values:
307
+ previous_old = @enum_old_hash[@tags[j]]
308
+ previous_new = @enum_new_hash[@tags[j]]
309
+ p_index = previous_old.length
310
+ if previous_old.index(current) == nil
311
+ # Current value has not been encountered before:
312
+ value = @values[j]+(p_index + 1).to_s
313
+ # Store value in array (and hash):
314
+ previous_old += [current]
315
+ previous_new += [value]
316
+ @enum_old_hash[@tags[j]] = previous_old
317
+ @enum_new_hash[@tags[j]] = previous_new
318
+ else
319
+ # Current value has been observed before:
320
+ value = previous_new[previous_old.index(current)]
321
+ end # of if previous.index...else..
322
+ else
323
+ value = @values[j]
324
+ end # of if @enum[j]..else..
325
+ return value
326
+ end # of method handle_enumeration
327
+
328
+
329
+ # Discover all the files contained in the specified directory and all its sub-directories:
330
+ def load_files()
331
+ # Load find library:
332
+ require 'find'
333
+ # Iterate through the folders (and its subfolders) to extract all files:
334
+ for dir in @folders
335
+ Find.find(dir) do |path|
336
+ if FileTest.directory?(path)
337
+ if @exceptions.include?(File.basename(path))
338
+ Find.prune # Don't look any further into this directory.
339
+ else
340
+ next
341
+ end
342
+ else
343
+ @files += [path] # Store the file in our array
344
+ end
345
+ end
346
+ end # of for dir...
347
+ end # of method load_files
348
+
349
+
350
+ # Analyses the write_path and the 'read' file path to determine if the have some common root.
351
+ # If there are parts of file that exist also in write path, it will not add those parts to write_path.
352
+ def process_write_paths()
353
+ # First make sure @write_path ends with a "/" (represented by decimal 47):
354
+ @write_path = @write_path + "/" unless @write_path[@write_path.length-1] == 47
355
+ # Separate behaviour if we have one, or several files in our array:
356
+ if @files.length == 1
357
+ # One file.
358
+ # Write path is requested write path + old file name:
359
+ str_arr = @files[0].split("/")
360
+ @write_paths += [@write_path + str_arr.last]
361
+ else
362
+ # Several files.
363
+ # Find out how much of the path they have in common, remove that and
364
+ # add the remaining to the @write_path:
365
+ str_arr = @files[0].split("/")
366
+ last_match_index = common_path(str_arr, 0)
367
+ if last_match_index >= 0
368
+ # Remove the matching folders from the path that will be added to @write_path:
369
+ @files.each do |file|
370
+ arr = file.split("/")
371
+ part_to_write = arr[(last_match_index+1)..(arr.length-1)].join("/")
372
+ @write_paths += [@write_path + part_to_write]
373
+ end
374
+ else
375
+ # No common folders. Add all of original path to write path:
376
+ @files.each do |file|
377
+ @write_paths += [@write_path + file]
378
+ end
379
+ end
380
+ end # of if @files.length..
381
+ end # of method process_write_paths
382
+
383
+
384
+ # Default tags that will be anonymized, along with some settings for each:
385
+ def set_defaults()
386
+ data = [
387
+ ["0008,0012", "20000101", false], # Instance Creation Date
388
+ ["0008,0013", "000000.00", false], # Instance Creation Time
389
+ ["0008,0020", "20000101", false], # Study Date
390
+ ["0008,0023", "20000101", false], # Image Date
391
+ ["0008,0030", "000000.00", false], # Study Time
392
+ ["0008,0033", "000000.00", false], # Image Time
393
+ ["0008,0080", "Institution", true], # Institution name
394
+ ["0008,0090", "Physician", true], # Referring Physician's name
395
+ ["0008,1010", "Station", true], # Station name
396
+ ["0010,0010", "Patient", true], # Patient's name
397
+ ["0010,0020", "ID", true], # Patient's ID
398
+ ["0010,0030", "20000101", false], # Patient's Birth Date
399
+ ["0010,0040", "N", false], # Patient's Sex
400
+ ["0020,4000", "", false], # Image Comments
401
+ ].transpose
402
+ @tags = data[0]
403
+ @values = data[1]
404
+ @enum = data[2]
405
+ end # of method set_defaults
406
+
407
+
408
+ # Writes an identity file, which allows reidentification of DICOM files that have been anonymized
409
+ # using the enumeration feature. Values will be saved in a text file, using semi colon delineation.
410
+ def write_identity_file()
411
+ # Open file and prepare to write text:
412
+ File.open( @identity_file, 'w' ) do |output|
413
+ # Cycle through each
414
+ @tags.each_index do |i|
415
+ if @enum[i]
416
+ # This tag has had enumeration. Gather original and anonymized values:
417
+ old_values = @enum_old_hash[@tags[i]]
418
+ new_values = @enum_new_hash[@tags[i]]
419
+ # Print the tag label, then new_value;old_value in the following rows.
420
+ output.print @tags[i] + "\n"
421
+ old_values.each_index do |j|
422
+ output.print new_values[j].to_s.rstrip + ";" + old_values[j].to_s.rstrip + "\n"
423
+ end
424
+ # Print empty line for separation between different tags:
425
+ output.print "\n"
426
+ end # of if @enum[i]
427
+ end # of @tags.each...
428
+ end # of File.open...
429
+ end # of method write...
430
+
431
+
432
+ end # of class
433
+ end # of module