dicom 0.8 → 0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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]]
|