dicom 0.8 → 0.9
Sign up to get free protection for your applications and to get access to all the features.
- data/{CHANGELOG → CHANGELOG.rdoc} +100 -52
- data/README.rdoc +126 -0
- data/lib/dicom.rb +13 -7
- data/lib/dicom/anonymizer.rb +129 -111
- data/lib/dicom/constants.rb +60 -10
- data/lib/dicom/d_client.rb +230 -157
- data/lib/dicom/d_library.rb +88 -8
- data/lib/dicom/d_object.rb +141 -149
- data/lib/dicom/d_read.rb +42 -36
- data/lib/dicom/d_server.rb +8 -10
- data/lib/dicom/d_write.rb +25 -46
- data/lib/dicom/dictionary.rb +1 -3
- data/lib/dicom/{data_element.rb → element.rb} +61 -49
- data/lib/dicom/elemental.rb +126 -0
- data/lib/dicom/file_handler.rb +18 -17
- data/lib/dicom/image_item.rb +844 -0
- data/lib/dicom/image_processor.rb +69 -0
- data/lib/dicom/image_processor_mini_magick.rb +74 -0
- data/lib/dicom/image_processor_r_magick.rb +102 -0
- data/lib/dicom/item.rb +21 -19
- data/lib/dicom/link.rb +64 -82
- data/lib/dicom/{super_parent.rb → parent.rb} +270 -39
- data/lib/dicom/ruby_extensions.rb +175 -3
- data/lib/dicom/sequence.rb +5 -6
- data/lib/dicom/stream.rb +37 -25
- data/lib/dicom/variables.rb +51 -0
- data/lib/dicom/version.rb +6 -0
- metadata +97 -29
- data/README +0 -100
- data/init.rb +0 -1
- data/lib/dicom/elements.rb +0 -82
- data/lib/dicom/super_item.rb +0 -696
@@ -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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
39
|
-
|
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
|
-
|
99
|
+
* Some minor text corrections.
|
42
100
|
* Anonymizer:
|
43
|
-
|
44
|
-
|
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
|
-
|
47
|
-
|
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
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
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
|
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.
|
data/README.rdoc
ADDED
@@ -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]
|
data/lib/dicom.rb
CHANGED
@@ -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
|
-
# *
|
6
|
-
# *
|
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/
|
17
|
-
require 'dicom/
|
18
|
-
require 'dicom/
|
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
|
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'
|
data/lib/dicom/anonymizer.rb
CHANGED
@@ -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
|
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
|
-
@
|
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
|
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
|
-
|
90
|
-
|
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
|
-
#
|
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
|
-
|
158
|
-
|
159
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
192
|
-
|
145
|
+
add_msg("*******************************************************")
|
146
|
+
add_msg("Initiating anonymization process.")
|
193
147
|
start_time = Time.now.to_f
|
194
|
-
|
148
|
+
add_msg("Searching for files...")
|
195
149
|
load_files
|
196
|
-
|
150
|
+
add_msg("Done.")
|
197
151
|
if @files.length > 0
|
198
152
|
if @tags.length > 0
|
199
|
-
|
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
|
-
|
156
|
+
add_msg("Processing write paths...")
|
203
157
|
process_write_paths
|
204
|
-
|
158
|
+
add_msg("Done")
|
205
159
|
else
|
206
160
|
# Overwriting old files:
|
207
|
-
|
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
|
-
|
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?(
|
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
|
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
|
-
#
|
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
|
-
|
217
|
+
add_msg("Anonymization process completed!")
|
267
218
|
if all_read
|
268
|
-
|
219
|
+
add_msg("All files in specified folder(s) were SUCCESSFULLY read to DICOM objects.")
|
269
220
|
else
|
270
|
-
|
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
|
-
|
224
|
+
add_msg("All DICOM objects were SUCCESSFULLY written as DICOM files (#{files_written} files).")
|
274
225
|
else
|
275
|
-
|
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
|
-
|
230
|
+
add_msg("Writing identity file.")
|
280
231
|
write_identity_file
|
281
|
-
|
232
|
+
add_msg("Done")
|
282
233
|
end
|
283
234
|
elapsed = (end_time-start_time).to_s
|
284
|
-
|
235
|
+
add_msg("Elapsed time: " + elapsed[0..elapsed.index(".")+1] + " seconds")
|
285
236
|
else
|
286
|
-
|
237
|
+
add_msg("No tags have been selected for anonymization. Aborting.")
|
287
238
|
end
|
288
239
|
else
|
289
|
-
|
240
|
+
add_msg("No files were found in specified folders. Aborting.")
|
290
241
|
end
|
291
|
-
|
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 = @
|
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
|
-
@
|
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
|
-
|
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
|
-
@
|
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 @
|
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
|
-
@
|
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(
|
556
|
+
File.open(@identity_file, 'w') do |output|
|
539
557
|
# Cycle through each
|
540
558
|
@tags.each_index do |i|
|
541
|
-
if @
|
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]]
|