dicom 0.3 → 0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +30 -4
- data/DOCUMENTATION +135 -14
- data/README +14 -5
- data/lib/Anonymizer.rb +433 -0
- data/lib/DLibrary.rb +29 -24
- data/lib/DObject.rb +779 -212
- data/lib/DRead.rb +410 -679
- data/lib/DWrite.rb +432 -0
- data/lib/Dictionary.rb +2795 -2805
- data/lib/dicom.rb +5 -1
- metadata +11 -9
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
|
47
|
-
* Reading
|
48
|
-
* Reading of multiple frame image data
|
49
|
-
* Retrieving images when file contains two or more unrelated images
|
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.
|
data/DOCUMENTATION
CHANGED
@@ -1,11 +1,14 @@
|
|
1
|
-
DICOM is a small library for reading
|
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
|
-
|
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,
|
32
|
+
new(filename, options={})
|
30
33
|
|
31
34
|
Initialize a new DICOM object.
|
32
|
-
Example 1 (The
|
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
|
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
|
-
|
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.
|
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.
|
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,
|
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
|
-
|
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
|
-
|
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
|
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
|
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.
|
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
|
|
data/lib/Anonymizer.rb
ADDED
@@ -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
|