dicom 0.8 → 0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,56 +1,114 @@
1
+ = 0.9
2
+
3
+ === 17th May, 2011
4
+
5
+ * Fixed an issue where data elements added to a DObject didn't get their endianness updated.
6
+ * Added the ability to encode/decode 32 bit pixel data.
7
+ * Added the ability to encode/decode big endian signed integers.
8
+ * Added the remove_children() method to parent elements.
9
+ * Added the ability to decode color images.
10
+ * Added the ability to retrieve multiple pixel frames as image objects.
11
+ * Added a :level option to the image retrieval methods which accepts custom window center/width values.
12
+ * Renamed the :rescale option to :remap in the image retrieval methods.
13
+ * Added the ability to decode RLE and JPEG Baseline compressed images.
14
+ * Added a specification test suite (using rspec and mocha).
15
+ * Implemented proper handling of data element values with VR "AT" (tag references).
16
+ * Changed the specification for Item indices (now starts at 0).
17
+ * Introduced the concept of ImageProcessors to replace the previously integrated RMagick image methods:
18
+ * The user can select which ImageProcessor to be used at run-time.
19
+ * Initially supported ImageProcessors: RMagick and mini_magick.
20
+ * Additional ImageProcessors can easily be added using existing template.
21
+ * Major refactoring and simplification of all image related code.
22
+ * Some changes of the Anonymizer class:
23
+ * Added a :verbose option.
24
+ * Added enum(), set_tag() and value() methods.
25
+ * Removed add_tag, change_enum() and change_value() methods.
26
+ * Added a log attribute.
27
+ * Fixed an issue where using a transfer syntax option with a binary string did not result in a correct application of the supplied syntax.
28
+ * Added a .dcm extension to DICOM files received by DServer.
29
+ * Fixed an issue where sending DICOM files with compressed pixel data resulted in an invalid DICOM transfer.
30
+ * Changed the specification to allow the query methods of DClient to accept any tag as a query parameter.
31
+ * Improved the handling of parent-child relations so these are always properly updated when adding, removing or changing parent elements.
32
+ * Renamed the following classes/modules:
33
+ * SuperParent -> Parent
34
+ * SuperItem -> ImageItem
35
+ * DataElement -> Element
36
+ * Elements -> Elemental
37
+ * Replaced ImageItem#image_properties() with num_cols(), num_rows() and num_frames().
38
+ * Renamed the following ImageItem methods:
39
+ * get_image_magick() -> image()
40
+ * get_images_magick() -> images()
41
+ * set_image_magick() -> image=()
42
+ * get_image() -> pixels
43
+ * get_image_narray -> narray
44
+ * set_image -> pixels=()
45
+ * set_image_narray -> pixels=()
46
+ * Added the following conversion methods to DObject, Sequence, Item & Element:
47
+ * inspect()
48
+ * to_hash()
49
+ * to_json()
50
+ * to_yaml()
51
+ * Added the ability to use DICOM element names as methods for creating, editing and deleting elements.
52
+ * Added the ability to read a DICOM file from a http string, using open-uri (experimental).
53
+ * Added alias_methods read? for read_success and written? for write_success in DObject.
54
+ * Renamed DObject#information() to summary().
55
+ * Added various ArgumentError exceptions to facility easier debugging.
56
+ * Various bug fixes.
57
+
58
+
1
59
  = 0.8
2
60
 
3
61
  === 1st August, 2010
4
62
 
5
63
  * Overall changes:
6
- * A complete rewrite of data element handling has resulted in a substantially different syntax and significantly cleaner and simpler code.
7
- * Greatly increased speed in DICOM object interaction. Especially noticeable on larger DICOM files.
8
- * A complete overhaul of the documentation has resulted in a consistent RDoc-style documentation across all classes/modules.
9
- * Increased use of module constants for improved code readability.
10
- * Major cleanup of codes and comments to make it consistent with Ruby 'best practice' guidelines.
11
- * Ready for Rails 3 (compatible with Bundler).
64
+ * A complete rewrite of data element handling has resulted in a substantially different syntax and significantly cleaner and simpler code.
65
+ * Greatly increased speed in DICOM object interaction. Especially noticeable on larger DICOM files.
66
+ * A complete overhaul of the documentation has resulted in a consistent RDoc-style documentation across all classes/modules.
67
+ * Increased use of module constants for improved code readability.
68
+ * Major cleanup of codes and comments to make it consistent with Ruby 'best practice' guidelines.
69
+ * Ready for Rails 3 (compatible with Bundler).
12
70
  * Resulting from the rewrite is a number of new classes and modules for handling the various data objects:
13
- * DataElement: Ordinary data elements are instances of this class.
14
- * Item: Item elements are instances of this class.
15
- * Sequence: Sequence elements are instances of this class.
16
- * SuperItem: Methods which are shared between DObject and Item (mostly image related methods).
17
- * SuperParent: Methods which are shared between all parent objects (DObject, Item & Sequence).
18
- * Elements: A mix-in module used by all element objects (DataElement, Item & Sequence).
71
+ * DataElement: Ordinary data elements are instances of this class.
72
+ * Item: Item elements are instances of this class.
73
+ * Sequence: Sequence elements are instances of this class.
74
+ * SuperItem: Methods which are shared between DObject and Item (mostly image related methods).
75
+ * SuperParent: Methods which are shared between all parent objects (DObject, Item & Sequence).
76
+ * Elements: A mix-in module used by all element objects (DataElement, Item & Sequence).
19
77
  * DObject:
20
- * Completely rewritten data element handling.
21
- * Rewritten print() method with improved tree structure visualization, item indices and the ability to run on any parent element.
22
- * Renamed the print_properties() method to information(), and improved the amount of information outputted.
23
- * Removed the following methods: get_frames(), get_image_pos(), get_pos(), get_value(), get_bin(), set_value()
24
- * Renamed get_pixels() method to decode_pixels().
25
- * Replaced get_value() with value(). The set_value() method is replaced by the new() methods of DataElement, Sequence and Item classes.
26
- * Added the ability to add items at a specific index (shifting any existing items to appear after the inserted item).
27
- * Added the transfer_syntax=() method for changing the transfer syntax of a DICOM object.
28
- * Implemented a more robust padding of data element values with odd length, based on data element VR.
29
- * Added methods for removing all sequences, removing selected groups and returning all data elements of a selected group.
30
- * Optimized the get_image_narray() method to more efficiently return the pixel data.
31
- * Refined the print_all convenience method to call both print() and information().
78
+ * Completely rewritten data element handling.
79
+ * Rewritten print() method with improved tree structure visualization, item indices and the ability to run on any parent element.
80
+ * Renamed the print_properties() method to information(), and improved the amount of information outputted.
81
+ * Removed the following methods: get_frames(), get_image_pos(), get_pos(), get_value(), get_bin(), set_value()
82
+ * Renamed get_pixels() method to decode_pixels().
83
+ * Replaced get_value() with value(). The set_value() method is replaced by the new() methods of DataElement, Sequence and Item classes.
84
+ * Added the ability to add items at a specific index (shifting any existing items to appear after the inserted item).
85
+ * Added the transfer_syntax=() method for changing the transfer syntax of a DICOM object.
86
+ * Implemented a more robust padding of data element values with odd length, based on data element VR.
87
+ * Added methods for removing all sequences, removing selected groups and returning all data elements of a selected group.
88
+ * Optimized the get_image_narray() method to more efficiently return the pixel data.
89
+ * Refined the print_all convenience method to call both print() and information().
32
90
  * DRead:
33
- * More robust against crashing when parsing invalid files by proper use of exception handling.
34
- * Fixed some cases where a failed read where incorrectly marked as successful.
35
- * Increased compatibility (handles DICOM files saved with explicit syntax but no transfer syntax tag).
36
- * Explicit DICOM files with data elements of type "OF" are now handled correctly (both read and write).
91
+ * More robust against crashing when parsing invalid files by proper use of exception handling.
92
+ * Fixed some cases where a failed read where incorrectly marked as successful.
93
+ * Increased compatibility (handles DICOM files saved with explicit syntax but no transfer syntax tag).
94
+ * Explicit DICOM files with data elements of type "OF" are now handled correctly (both read and write).
37
95
  * DWrite:
38
- * Simplified code and execution by removing group lengths (deprecated in the DICOM standard) and using undefined length for sequences/items.
39
- * More flexible insertion of missing meta group elements.
96
+ * Simplified code and execution by removing group lengths (deprecated in the DICOM standard) and using undefined length for sequences/items.
97
+ * More flexible insertion of missing meta group elements.
40
98
  * Dictionary:
41
- * Some minor text corrections.
99
+ * Some minor text corrections.
42
100
  * Anonymizer:
43
- * Anonymizations executes much faster thanks to the work done with DObject data element handling.
44
- * Only top level data elements can be anonymized.
101
+ * Anonymizations executes much faster thanks to the work done with DObject data element handling.
102
+ * Only top level data elements can be anonymized.
45
103
  * DClient:
46
- * Can transmit multiple DICOM files at once with the send() method, which now will accept both DObjects and file strings.
47
- * Added the echo() method for performing a C-ECHO-RQ against a SCP.
104
+ * Can transmit multiple DICOM files at once with the send() method, which now will accept both DObjects and file strings.
105
+ * Added the echo() method for performing a C-ECHO-RQ against a SCP.
48
106
  * DServer:
49
- * Support for role negotiation.
50
- * Properly returns called AE title in association response.
51
- * The lists of acceptable abstract and transfer syntax can now be easily modified by the user.
52
- * Receiving multiple files (like an entire series, or study) in a single association is now supported.
53
- * Properly receives and responds to a C-ECHO-RQ.
107
+ * Support for role negotiation.
108
+ * Properly returns called AE title in association response.
109
+ * The lists of acceptable abstract and transfer syntax can now be easily modified by the user.
110
+ * Receiving multiple files (like an entire series, or study) in a single association is now supported.
111
+ * Properly receives and responds to a C-ECHO-RQ.
54
112
 
55
113
 
56
114
  = 0.7
@@ -102,7 +160,7 @@
102
160
 
103
161
  * Complete rewrite of encoding/decoding to reduce code duplication and simplify the code.
104
162
  * General optimizations to improve speed and reduce code complexity.
105
- * Rewrote Dictionary to use Hash instead of Array. This has significantly improved the read spead of the library.
163
+ * Rewrote Dictionary to use Hash instead of Array. This has significantly improved the read speed of the library.
106
164
  * Ruby DICOM now automatically strips away trailing whitespace when decoding string values.
107
165
  * Introducing basic, experimental network functionality.
108
166
  * The new DClient class enables features such as query, move and send.
@@ -188,14 +246,4 @@
188
246
  First public release.
189
247
  The library does have several known issues and lacks some features I would like it to have, but it does
190
248
  offer basic functionality and should be usable for people interested in working with DICOM files in Ruby.
191
- The reading algorithm has been tested succesfully with some 40 different DICOM files, so it should be fairly robust.
192
-
193
-
194
- Known issues:
195
- * The retrieve file network functionality (get_image() in DClient class) has not been tested.
196
- * Compressed pixel data is poorly handled.
197
- * Read/Write 12 bit image data not available.
198
- * Color image data is poorly handled.
199
- * Incomplete support for Big endian (Everything but signed short and signed long has been implemented).
200
- * Incomplete support for multiple frame image data to NArray and RMagick objects (partial support already featured).
201
- * Image handling does not take into consideration DICOM tags which specify orientation, samples per pixel and photometric interpretation.
249
+ The reading algorithm has been tested successfully with some 40 different DICOM files, so it should be fairly robust.
@@ -0,0 +1,126 @@
1
+ = RUBY DICOM
2
+
3
+ Ruby DICOM is a small and simple library for handling DICOM in Ruby. DICOM (Digital Imaging
4
+ and Communications in Medicine) is a standard for handling, storing, printing,
5
+ and transmitting information in medical imaging. It includes a file format definition
6
+ and a network communications protocol. Ruby DICOM supports reading from, editing
7
+ and writing to this file format. It also features basic support for select network
8
+ communication modalities like querying, moving, sending and receiving files.
9
+
10
+
11
+ == INSTALLATION
12
+
13
+ gem install dicom
14
+
15
+
16
+ == BASIC USAGE
17
+
18
+ === Load & Include
19
+
20
+ require 'dicom'
21
+ include DICOM
22
+
23
+ === Read, modify and write
24
+
25
+ # Read file:
26
+ obj = DObject.new("some_file.dcm")
27
+ # Extract the Patient's Name value:
28
+ obj.patients_name.value
29
+ # Add or modify the Patient's Name element:
30
+ obj.patients_name = "Anonymous"
31
+ # Remove a data element from the DICOM object:
32
+ obj.pixel_data = nil
33
+ # Write to file:
34
+ obj.write("new_file.dcm")
35
+
36
+ === Modify using tag strings instead of dictionary method names
37
+
38
+ # Extract the Patient's Name value:
39
+ obj.value("0010,0010")
40
+ # Modify the Patient's Name element:
41
+ obj["0010,0010"].value = "Anonymous"
42
+ # Remove a data element from the DICOM object:
43
+ obj.remove("7FE0,0010")
44
+
45
+ === Extracting information about the DICOM object
46
+
47
+ # Display a short summary of the file's properties:
48
+ obj.summary
49
+ # Print all data elements to screen:
50
+ obj.print
51
+ # Convert the data element hierarchy to a nested hash:
52
+ obj.to_hash
53
+
54
+ === Handle pixel data
55
+
56
+ # Retrieve the pixel data in a Ruby Array:
57
+ obj.pixels
58
+ # Load the pixel data to an numerical array (NArray):
59
+ obj.narray
60
+ # Load the pixel data to an RMagick image object and display it on the screen:
61
+ obj.image.display
62
+
63
+ === Transmit a DICOM file
64
+
65
+ # Send a local file to a server (PACS) over the network:
66
+ node = DClient.new("10.1.25.200", 104)
67
+ node.send("some_file.dcm")
68
+
69
+ === Start a DICOM server
70
+
71
+ # Initiate a simple storage provider which can receive DICOM files of all modalities:
72
+ s = DServer.new(104, :host_ae => "MY_DICOM_SERVER")
73
+ s.start_scp("C:/temp/")
74
+
75
+ === IRB Tip
76
+
77
+ When working with Ruby DICOM in irb, you may be annoyed with all the information
78
+ that is printed to screen, regardless if you have set verbose as false. This is because
79
+ in irb every variable loaded in the program is automatically printed to the screen.
80
+ A useful hack to avoid this effect is to append ";0" after a command.
81
+ Example:
82
+ obj = DObject.new("some_file.dcm") ;0
83
+
84
+
85
+ == RESOURCES
86
+
87
+ * {Official home page}[http://dicom.rubyforge.org/]
88
+ * {Discussion forum}[http://groups.google.com/group/ruby-dicom]
89
+ * {Source code repository}[https://github.com/dicom/ruby-dicom]
90
+
91
+
92
+ == COPYRIGHT
93
+
94
+ Copyright 2008-2011 Christoffer Lervåg
95
+
96
+ This program is free software: you can redistribute it and/or modify
97
+ it under the terms of the GNU General Public License as published by
98
+ the Free Software Foundation, either version 3 of the License, or
99
+ (at your option) any later version.
100
+
101
+ This program is distributed in the hope that it will be useful,
102
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
103
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
104
+ GNU General Public License for more details.
105
+
106
+ You should have received a copy of the GNU General Public License
107
+ along with this program. If not, see http://www.gnu.org/licenses/ .
108
+
109
+
110
+ == ABOUT THE AUTHOR
111
+
112
+ * Name: Christoffer Lervåg
113
+ * Location: Norway
114
+ * Email: chris.lervag [@nospam.com] @gmail.com
115
+
116
+ Please don't hesitate to email me if you have any feedback related to this project!
117
+
118
+
119
+ == CONTRIBUTORS
120
+
121
+ * {Christoffer Lervåg}[https://github.com/dicom]
122
+ * {John Axel Eriksson}[https://github.com/johnae]
123
+ * {Donnie Millar}[https://github.com/dmillar]
124
+ * Björn Albers
125
+ * {Lars Benner}[https://github.com/Maturin]
126
+ * {Steven Bedrick}[https://github.com/stevenbedrick]
@@ -2,8 +2,8 @@
2
2
  #
3
3
  # The following classes are meant to be used by users of Ruby DICOM:
4
4
  # * DObject - for reading, manipulating and writing DICOM files.
5
- # * DataElement, Sequence, Item, SuperParent, Elements - users who wish to interact with their DICOM objects will use these classes/modules.
6
- # * SuperItem - Image related methods are found in this class.
5
+ # * Element, Sequence, Item, Parent, Elemental - users who wish to interact with their DICOM objects will use these classes/modules.
6
+ # * ImageItem - Image related methods are found in this class.
7
7
  # * DClient - for client side network communication, like querying, moving & sending DICOM files.
8
8
  # * DServer - for server side network communication: Setting up your own DICOM storage node (SCP).
9
9
  # * Anonymizer - a convenience class for anonymizing your DICOM files.
@@ -13,11 +13,11 @@
13
13
 
14
14
  # Core library:
15
15
  # Super classes/modules:
16
- require 'dicom/super_parent'
17
- require 'dicom/super_item'
18
- require 'dicom/elements'
16
+ require 'dicom/image_processor'
17
+ require 'dicom/parent'
18
+ require 'dicom/image_item'
19
+ require 'dicom/elemental'
19
20
  # Subclasses and independent classes:
20
- require 'dicom/data_element'
21
21
  require 'dicom/d_client'
22
22
  require 'dicom/dictionary'
23
23
  require 'dicom/d_library'
@@ -25,6 +25,7 @@ require 'dicom/d_object'
25
25
  require 'dicom/d_read'
26
26
  require 'dicom/d_server'
27
27
  require 'dicom/d_write'
28
+ require 'dicom/element'
28
29
  require 'dicom/file_handler'
29
30
  require 'dicom/item'
30
31
  require 'dicom/link'
@@ -32,8 +33,13 @@ require 'dicom/sequence'
32
33
  require 'dicom/stream'
33
34
  # Extensions to the Ruby library:
34
35
  require 'dicom/ruby_extensions'
35
- # Module constants:
36
+ # Module settings:
37
+ require 'dicom/version'
36
38
  require 'dicom/constants'
39
+ require 'dicom/variables'
40
+ # Image processors:
41
+ require 'dicom/image_processor_mini_magick'
42
+ require 'dicom/image_processor_r_magick'
37
43
 
38
44
  # Extensions (non-core functionality):
39
45
  require 'dicom/anonymizer'
@@ -1,4 +1,3 @@
1
- # Copyright 2008-2010 Christoffer Lervag
2
1
 
3
2
  module DICOM
4
3
 
@@ -16,8 +15,10 @@ module DICOM
16
15
  attr_accessor :blank
17
16
  # A boolean that if set as true will cause all anonymized tags to be get enumerated values, to enable post-anonymization identification by the user.
18
17
  attr_accessor :enumeration
19
- # A boolean, which if enumeration has been selected, can be set as true to make the anonymization produce an identity file that will provide a relationship between original and anonymized values.
18
+ # A string, which if set (and enumeration has been set as well), will make the Anonymizer produce an identity file that provides a relationship between the original and enumerated, anonymized values.
20
19
  attr_accessor :identity_file
20
+ # An array containing status messages accumulated for the Anonymization instance.
21
+ attr_accessor :log
21
22
  # A boolean that if set as true, will make the anonymization remove all private tags.
22
23
  attr_accessor :remove_private
23
24
  # The path where the anonymized files will be saved. If this value is not set, the original DICOM files will be overwritten.
@@ -25,11 +26,23 @@ module DICOM
25
26
 
26
27
  # Creates an Anonymizer instance.
27
28
  #
29
+ # === Parameters
30
+ #
31
+ # * <tt>options</tt> -- A hash of parameters.
32
+ #
33
+ # === Options
34
+ #
35
+ # * <tt>:verbose</tt> -- Boolean. If set to false, the Anonymizer instance will run silently and not output status updates to the screen. Defaults to true.
36
+ #
28
37
  # === Examples
29
38
  #
30
39
  # a = Anonymizer.new
40
+ # # Create an instance in non-verbose mode:
41
+ # a = Anonymizer.new(:verbose => false)
31
42
  #
32
- def initialize
43
+ def initialize(options={})
44
+ # Default verbosity is true if verbosity hasn't been specified (nil):
45
+ @verbose = (options[:verbose] == false ? false : true)
33
46
  # Default value of accessors:
34
47
  @blank = false
35
48
  @enumeration = false
@@ -45,7 +58,7 @@ module DICOM
45
58
  # Default values to use on anonymized data elements:
46
59
  @values = Array.new
47
60
  # Which data elements will have enumeration applied, if requested by the user:
48
- @enum = Array.new
61
+ @enumerations = Array.new
49
62
  # We use a Hash to store information from DICOM files if enumeration is desired:
50
63
  @enum_old_hash = Hash.new
51
64
  @enum_new_hash = Hash.new
@@ -53,6 +66,8 @@ module DICOM
53
66
  @files = Array.new
54
67
  # Write paths will be determined later and put in this array:
55
68
  @write_paths = Array.new
69
+ # Keep track of status messages:
70
+ @log = Array.new
56
71
  # Set the default data elements to be anonymized:
57
72
  set_defaults
58
73
  end
@@ -68,10 +83,11 @@ module DICOM
68
83
  # a.add_exception("/home/dicom/tutorials/")
69
84
  #
70
85
  def add_exception(path)
86
+ raise ArgumentError, "Expected String, got #{path.class}." unless path.is_a?(String)
71
87
  if path
72
88
  # Remove last character if the path ends with a file separator:
73
89
  path.chop! if path[-1..-1] == File::SEPARATOR
74
- @exceptions << path if path
90
+ @exceptions << path
75
91
  end
76
92
  end
77
93
 
@@ -86,88 +102,26 @@ module DICOM
86
102
  # a.add_folder("/home/dicom")
87
103
  #
88
104
  def add_folder(path)
89
- @folders << path if path
90
- end
91
-
92
- # Adds a tag to the list of tags that will be anonymized.
93
- #
94
- # === Parameters
95
- #
96
- # * <tt>tag</tt> -- String. A data element tag.
97
- # * <tt>options</tt> -- A hash of parameters.
98
- #
99
- # === Options
100
- #
101
- # * <tt>:value</tt> -- A replacement value to use for when anonymizing this data element.
102
- # * <tt>:enum</tt> -- Boolean. Specifies if enumeration is to be used for this tag (true) or not (false).
103
- #
104
- # === Examples
105
- #
106
- # a.add_tag("0010,0010, :value => "MrAnonymous", :enum => true)
107
- #
108
- def add_tag(tag, options={})
109
- # Options and defaults:
110
- value = options[:value] || ""
111
- enum = options[:enum] || false
112
- if tag
113
- if tag.is_a?(String)
114
- if tag.length == 9
115
- # Add anonymization information for this tag:
116
- @tags << tag
117
- @values << value
118
- @enum << enum
119
- else
120
- puts "Warning: Invalid tag length. Please use the form 'GGGG,EEEE'."
121
- end
122
- else
123
- puts "Warning: Tag is not a string. Can not add tag."
124
- end
125
- else
126
- puts "Warning: No tag supplied. Nothing to add."
127
- end
128
- end
129
-
130
- # Sets the enumeration status for a specific tag.
131
- #
132
- # === Parameters
133
- #
134
- # * <tt>tag</tt> -- String. A data element tag.
135
- # * <tt>enum</tt> -- Boolean. True to enable enumeration, false to disable it.
136
- #
137
- def change_enum(tag, enum)
138
- pos = @tags.index(tag)
139
- if pos
140
- if enum
141
- @enum[pos] = true
142
- else
143
- @enum[pos] = false
144
- end
145
- else
146
- puts "Specified tag not found in anonymization array. No changes made."
147
- end
105
+ raise ArgumentError, "Expected String, got #{path.class}." unless path.is_a?(String)
106
+ @folders << path
148
107
  end
149
108
 
150
- # Changes the value to be used in the anonymization of a specific tag.
109
+ # Returns the enumeration status for this tag.
110
+ # Returns nil if no match is found for the provided tag.
151
111
  #
152
112
  # === Parameters
153
113
  #
154
114
  # * <tt>tag</tt> -- String. A data element tag.
155
- # * <tt>value</tt> -- The new anonymization replacement value for this data element.
156
115
  #
157
- # === Examples
158
- #
159
- # a.change_value("0008,0090", "Dr.No")
160
- #
161
- def change_value(tag, value)
116
+ def enum(tag)
117
+ raise ArgumentError, "Expected String, got #{tag.class}." unless tag.is_a?(String)
118
+ raise ArgumentError, "Expected a valid tag of format 'GGGG,EEEE', got #{tag}." unless tag.tag?
162
119
  pos = @tags.index(tag)
163
120
  if pos
164
- if value
165
- @values[pos] = value
166
- else
167
- puts "No value were specified. No changes made."
168
- end
121
+ return @enumerations[pos]
169
122
  else
170
- puts "Specified tag not found in anonymization array. No changes made."
123
+ add_msg("The specified tag is not found in the list of tags to be anonymized.")
124
+ return nil
171
125
  end
172
126
  end
173
127
 
@@ -188,30 +142,30 @@ module DICOM
188
142
  #
189
143
  def execute(verbose=false)
190
144
  # Search through the folders to gather all the files to be anonymized:
191
- puts "*******************************************************"
192
- puts "Initiating anonymization process."
145
+ add_msg("*******************************************************")
146
+ add_msg("Initiating anonymization process.")
193
147
  start_time = Time.now.to_f
194
- puts "Searching for files..."
148
+ add_msg("Searching for files...")
195
149
  load_files
196
- puts "Done."
150
+ add_msg("Done.")
197
151
  if @files.length > 0
198
152
  if @tags.length > 0
199
- puts @files.length.to_s + " files have been identified in the specified folder(s)."
153
+ add_msg(@files.length.to_s + " files have been identified in the specified folder(s).")
200
154
  if @write_path
201
155
  # Determine the write paths, as anonymized files will be written to a separate location:
202
- puts "Processing write paths..."
156
+ add_msg("Processing write paths...")
203
157
  process_write_paths
204
- puts "Done"
158
+ add_msg("Done")
205
159
  else
206
160
  # Overwriting old files:
207
- puts "Separate write folder not specified. Will overwrite existing DICOM files."
161
+ add_msg("Separate write folder not specified. Will overwrite existing DICOM files.")
208
162
  @write_paths = @files
209
163
  end
210
164
  # If the user wants enumeration, we need to prepare variables for storing
211
165
  # existing information associated with each tag:
212
166
  create_enum_hash if @enumeration
213
167
  # Start the read/update/write process:
214
- puts "Initiating read/update/write process (This may take some time)..."
168
+ add_msg("Initiating read/update/write process (This may take some time)...")
215
169
  # Monitor whether every file read/write was successful:
216
170
  all_read = true
217
171
  all_write = true
@@ -225,25 +179,22 @@ module DICOM
225
179
  @tags.each_index do |j|
226
180
  if obj.exists?(@tags[j])
227
181
  element = obj[@tags[j]]
228
- if element.is_a?(DataElement)
182
+ if element.is_a?(Element)
229
183
  if @blank
230
184
  value = ""
231
185
  elsif @enumeration
232
186
  old_value = element.value
233
- # Only launch enumeration logic if tag exists:
187
+ # Only launch enumeration logic if there is an actual value to the data element:
234
188
  if old_value
235
189
  value = get_enumeration_value(old_value, j)
236
190
  else
237
191
  value = ""
238
192
  end
239
193
  else
240
- # Value is simply value in array:
194
+ # Use the value that has been set for this tag:
241
195
  value = @values[j]
242
196
  end
243
197
  element.value = value
244
- elsif element.is_a?(Item)
245
- # Possibly a binary data item:
246
- element.bin = ""
247
198
  end
248
199
  end
249
200
  end
@@ -263,32 +214,32 @@ module DICOM
263
214
  end
264
215
  # Finished anonymizing files. Print elapsed time and status of anonymization:
265
216
  end_time = Time.now.to_f
266
- puts "Anonymization process completed!"
217
+ add_msg("Anonymization process completed!")
267
218
  if all_read
268
- puts "All files in specified folder(s) were SUCCESSFULLY read to DICOM objects."
219
+ add_msg("All files in specified folder(s) were SUCCESSFULLY read to DICOM objects.")
269
220
  else
270
- puts "Some files were NOT successfully read (#{files_failed_read} files). If folder(s) contain non-DICOM files, this is probably the reason."
221
+ add_msg("Some files were NOT successfully read (#{files_failed_read} files). If folder(s) contain non-DICOM files, this is probably the reason.")
271
222
  end
272
223
  if all_write
273
- puts "All DICOM objects were SUCCESSFULLY written as DICOM files (#{files_written} files)."
224
+ add_msg("All DICOM objects were SUCCESSFULLY written as DICOM files (#{files_written} files).")
274
225
  else
275
- puts "Some DICOM objects were NOT succesfully written to file. You are advised to have a closer look (#{files_written} files succesfully written)."
226
+ add_msg("Some DICOM objects were NOT succesfully written to file. You are advised to have a closer look (#{files_written} files succesfully written).")
276
227
  end
277
228
  # Has user requested enumeration and specified an identity file in which to store the anonymized values?
278
229
  if @enumeration and @identity_file
279
- puts "Writing identity file."
230
+ add_msg("Writing identity file.")
280
231
  write_identity_file
281
- puts "Done"
232
+ add_msg("Done")
282
233
  end
283
234
  elapsed = (end_time-start_time).to_s
284
- puts "Elapsed time: " + elapsed[0..elapsed.index(".")+1] + " seconds"
235
+ add_msg("Elapsed time: " + elapsed[0..elapsed.index(".")+1] + " seconds")
285
236
  else
286
- puts "No tags have been selected for anonymization. Aborting."
237
+ add_msg("No tags have been selected for anonymization. Aborting.")
287
238
  end
288
239
  else
289
- puts "No files were found in specified folders. Aborting."
240
+ add_msg("No files were found in specified folders. Aborting.")
290
241
  end
291
- puts "*******************************************************"
242
+ add_msg("*******************************************************")
292
243
  end
293
244
 
294
245
  # Prints to screen a list of which tags are currently selected for anonymization along with
@@ -328,7 +279,7 @@ module DICOM
328
279
  f4 = " " if @blank
329
280
  f4 = " "*(value_maxL-@values[i].to_s.length+1) unless @blank
330
281
  if @enumeration
331
- enum = @enum[i]
282
+ enum = @enumerations[i]
332
283
  else
333
284
  enum = ""
334
285
  end
@@ -357,13 +308,67 @@ module DICOM
357
308
  # a.remove_tag("0010,0010")
358
309
  #
359
310
  def remove_tag(tag)
311
+ raise ArgumentError, "Expected String, got #{tag.class}." unless tag.is_a?(String)
312
+ raise ArgumentError, "Expected a valid tag of format 'GGGG,EEEE', got #{tag}." unless tag.tag?
360
313
  pos = @tags.index(tag)
361
314
  if pos
362
315
  @tags.delete_at(pos)
363
316
  @values.delete_at(pos)
364
- @enum.delete_at(pos)
317
+ @enumerations.delete_at(pos)
318
+ end
319
+ end
320
+
321
+ # Sets the anonymization settings for the specified tag. If the tag is already present in the list
322
+ # of tags to be anonymized, its settings are updated, and if not, a new tag entry is created.
323
+ #
324
+ # === Parameters
325
+ #
326
+ # * <tt>tag</tt> -- String. A data element tag.
327
+ # * <tt>options</tt> -- A hash of parameters.
328
+ #
329
+ # === Options
330
+ #
331
+ # * <tt>:value</tt> -- The replacement value to be used when anonymizing this data element. Defaults to the pre-existing value and "" for new tags.
332
+ # * <tt>:enum</tt> -- Boolean. Specifies if enumeration is to be used for this tag. Defaults to the pre-existing value and false for new tags.
333
+ #
334
+ # === Examples
335
+ #
336
+ # a.set_tag("0010,0010, :value => "MrAnonymous", :enum => true)
337
+ #
338
+ def set_tag(tag, options={})
339
+ raise ArgumentError, "Expected String, got #{tag.class}." unless tag.is_a?(String)
340
+ raise ArgumentError, "Expected a valid tag of format 'GGGG,EEEE', got #{tag}." unless tag.tag?
341
+ pos = @tags.index(tag)
342
+ if pos
343
+ # Update existing values:
344
+ @values[pos] = options[:value] if options[:value]
345
+ @enumerations[pos] = options[:enum] if options[:enum] != nil
365
346
  else
366
- puts "Specified tag not found in anonymization array. No changes made."
347
+ # Add new elements:
348
+ @tags << tag
349
+ @values << (options[:value] ? options[:value] : "")
350
+ @enumerations << (options[:enum] ? options[:enum] : false)
351
+ end
352
+ end
353
+
354
+ # Returns the value which will be used when anonymizing this tag.
355
+ # If enumeration is selected for the particular tag, a number will be
356
+ # appended in addition to the string that is returned here.
357
+ # Returns nil if no match is found for the provided tag.
358
+ #
359
+ # === Parameters
360
+ #
361
+ # * <tt>tag</tt> -- String. A data element tag.
362
+ #
363
+ def value(tag)
364
+ raise ArgumentError, "Expected String, got #{tag.class}." unless tag.is_a?(String)
365
+ raise ArgumentError, "Expected a valid tag of format 'GGGG,EEEE', got #{tag}." unless tag.tag?
366
+ pos = @tags.index(tag)
367
+ if pos
368
+ return @values[pos]
369
+ else
370
+ add_msg("The specified tag is not found in the list of tags to be anonymized.")
371
+ return nil
367
372
  end
368
373
  end
369
374
 
@@ -372,6 +377,18 @@ module DICOM
372
377
  private
373
378
 
374
379
 
380
+ # Adds one or more status messages to the log instance array, and if the verbose
381
+ # instance variable is true, the status message is printed to the screen as well.
382
+ #
383
+ # === Parameters
384
+ #
385
+ # * <tt>msg</tt> -- Status message string.
386
+ #
387
+ def add_msg(msg)
388
+ puts msg if @verbose
389
+ @log << msg
390
+ end
391
+
375
392
  # Finds the common path (if any) in the instance file path array, by performing a recursive search
376
393
  # on the folders that make up the path of one such file.
377
394
  # Returns the index of the last folder in the path of the selected file that is common for all file paths.
@@ -402,7 +419,7 @@ module DICOM
402
419
  # Creates a hash that is used for storing information that is used when enumeration is selected.
403
420
  #
404
421
  def create_enum_hash
405
- @enum.each_index do |i|
422
+ @enumerations.each_index do |i|
406
423
  @enum_old_hash[@tags[i]] = Array.new
407
424
  @enum_new_hash[@tags[i]] = Array.new
408
425
  end
@@ -420,7 +437,7 @@ module DICOM
420
437
  #
421
438
  def get_enumeration_value(current, j)
422
439
  # Is enumeration requested for this tag?
423
- if @enum[j]
440
+ if @enumerations[j]
424
441
  # Retrieve earlier used anonymization values:
425
442
  previous_old = @enum_old_hash[@tags[j]]
426
443
  previous_new = @enum_new_hash[@tags[j]]
@@ -527,18 +544,19 @@ module DICOM
527
544
  ].transpose
528
545
  @tags = data[0]
529
546
  @values = data[1]
530
- @enum = data[2]
547
+ @enumerations = data[2]
531
548
  end
532
549
 
533
550
  # Writes an identity file, which allows reidentification of DICOM files that have been anonymized
534
551
  # using the enumeration feature. Values are saved in a text file, using semi colon delineation.
535
552
  #
536
553
  def write_identity_file
554
+ raise ArgumentError, "Expected String, got #{@identity_file.class}. Unable to write identity file." unless @identity_file.is_a?(String)
537
555
  # Open file and prepare to write text:
538
- File.open( @identity_file, 'w' ) do |output|
556
+ File.open(@identity_file, 'w') do |output|
539
557
  # Cycle through each
540
558
  @tags.each_index do |i|
541
- if @enum[i]
559
+ if @enumerations[i]
542
560
  # This tag has had enumeration. Gather original and anonymized values:
543
561
  old_values = @enum_old_hash[@tags[i]]
544
562
  new_values = @enum_new_hash[@tags[i]]