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
data/lib/dicom/constants.rb
CHANGED
@@ -1,18 +1,10 @@
|
|
1
|
-
# Copyright 2010 Christoffer Lervag
|
2
|
-
|
3
|
-
# This file contains module constants used by the Ruby DICOM library.
|
4
1
|
|
5
2
|
module DICOM
|
6
3
|
|
7
|
-
# Ruby DICOM
|
8
|
-
VERSION = "0.8"
|
9
|
-
|
10
|
-
# Ruby DICOM's implementation UID.
|
4
|
+
# Ruby DICOM's Implementation UID.
|
11
5
|
UID = "1.2.826.0.1.3680043.8.641"
|
12
6
|
# Ruby DICOM name & version (max 16 characters).
|
13
7
|
NAME = "RUBY_DCM_" + DICOM::VERSION
|
14
|
-
# Application title.
|
15
|
-
SOURCE_APP_TITLE = "RUBY_DICOM"
|
16
8
|
|
17
9
|
# Item tag.
|
18
10
|
ITEM_TAG = "FFFE,E000"
|
@@ -115,7 +107,65 @@ module DICOM
|
|
115
107
|
# System (CPU) Endianness.
|
116
108
|
CPU_ENDIAN = endian_type[Array(x).pack("L*")]
|
117
109
|
|
110
|
+
# Custom string used for (un)packing big endian signed short.
|
111
|
+
CUSTOM_SS = "k*"
|
112
|
+
# Custom string used for (un)packing big endian signed long.
|
113
|
+
CUSTOM_SL = "r*"
|
114
|
+
|
118
115
|
# Ruby DICOM's library (data dictionary).
|
119
116
|
LIBRARY = DICOM::DLibrary.new
|
120
117
|
|
121
|
-
|
118
|
+
# Transfer Syntaxes
|
119
|
+
# Taken from DICOM Specification PS 3.5, Chapter 10
|
120
|
+
|
121
|
+
# General
|
122
|
+
TXS_IMPLICIT_LITTLE_ENDIAN = '1.2.840.10008.1.2' # also defined as IMPLICIT_LITTLE_ENDIAN, default transfer syntax
|
123
|
+
TXS_EXPLICIT_LITTLE_ENDIAN = '1.2.840.10008.1.2.1' # also defined as EXPLICIT_LITTLE_ENDIAN
|
124
|
+
TXS_EXPLICIT_BIG_ENDIAN = '1.2.840.10008.1.2.2' # also defined as EXPLICIT_BIG_ENDIAN
|
125
|
+
|
126
|
+
# TRANSFER SYNTAXES FOR ENCAPSULATION OF ENCODED PIXEL DATA
|
127
|
+
TXS_JPEG_BASELINE = '1.2.840.10008.1.2.4.50'
|
128
|
+
TXS_JPEG_EXTENDED = '1.2.840.10008.1.2.4.51'
|
129
|
+
TXS_JPEG_LOSSLESS_NH = '1.2.840.10008.1.2.4.57' # NH: non-hirarchical
|
130
|
+
TXS_JPEG_LOSSLESS_NH_FOP = '1.2.840.10008.1.2.4.70' # NH: non-hirarchical, FOP: first-order prediction
|
131
|
+
|
132
|
+
TXS_JPEG_LS_LOSSLESS = '1.2.840.10008.1.2.4.80'
|
133
|
+
TXS_JPEG_LS_NEAR_LOSSLESS = '1.2.840.10008.1.2.4.81'
|
134
|
+
|
135
|
+
TXS_JPEG_2000_PART1_LOSSLESS = '1.2.840.10008.1.2.4.90'
|
136
|
+
TXS_JPEG_2000_PART1_LOSSLESS_OR_LOSSY = '1.2.840.10008.1.2.4.91'
|
137
|
+
TXS_JPEG_2000_PART2_LOSSLESS = '1.2.840.10008.1.2.4.92'
|
138
|
+
TXS_JPEG_2000_PART2_LOSSLESS_OR_LOSSY = '1.2.840.10008.1.2.4.93'
|
139
|
+
|
140
|
+
TXS_MPEG2_MP_ML = '1.2.840.10008.1.2.4.100'
|
141
|
+
TXS_MPEG2_MP_HL = '1.2.840.10008.1.2.4.101'
|
142
|
+
|
143
|
+
TXS_DEFLATED_LITTLE_ENDIAN = '1.2.840.10008.1.2.1.99' # ZIP Compression
|
144
|
+
|
145
|
+
TXS_JPIP = '1.2.840.10008.1.2.4.94'
|
146
|
+
TXS_JPIP_DEFLATE = '1.2.840.10008.1.2.4.95'
|
147
|
+
|
148
|
+
TXS_RLE = '1.2.840.10008.1.2.5'
|
149
|
+
|
150
|
+
|
151
|
+
# Photometric Interpretations
|
152
|
+
# Taken from DICOM Specification PS 3.3 C.7.6.3.1.2 Photometric Interpretation
|
153
|
+
|
154
|
+
PI_MONOCHROME1 = 'MONOCHROME1'
|
155
|
+
PI_MONOCHROME2 = 'MONOCHROME2'
|
156
|
+
PI_PALETTE_COLOR = 'PALETTE COLOR'
|
157
|
+
PI_RGB = 'RGB'
|
158
|
+
PI_YBR_FULL = 'YBR_FULL'
|
159
|
+
PI_YBR_FULL_422 = 'YBR_FULL_422 '
|
160
|
+
PI_YBR_PARTIAL_422 = 'YBR_PARTIAL_422'
|
161
|
+
PI_YBR_PARTIAL_420 = 'YBR_PARTIAL_420'
|
162
|
+
PI_YBR_ICT = 'YBR_ICT'
|
163
|
+
PI_YBR_RCT = 'YBR_RCT'
|
164
|
+
|
165
|
+
# Retired Photometric Interpretations, are those needed to be supported?
|
166
|
+
PI_HSV = 'HSV'
|
167
|
+
PI_ARGB = 'ARGB'
|
168
|
+
PI_CMYK = 'CMYK'
|
169
|
+
|
170
|
+
end
|
171
|
+
|
data/lib/dicom/d_client.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
-
# Copyright 2009-2010 Christoffer Lervag
|
2
1
|
|
3
2
|
module DICOM
|
4
3
|
|
5
4
|
# This class contains code for handling the client side of DICOM TCP/IP network communication.
|
6
5
|
#
|
6
|
+
# === Resources
|
7
|
+
#
|
8
|
+
# * For information regarding query, such as required/optional attributes and value matching, refer to the DICOM standard, PS3.4, C 2.2.
|
7
9
|
#--
|
8
10
|
# FIXME: The code which waits for incoming network packets seems to be very CPU intensive. Perhaps there is a more elegant way to wait for incoming messages?
|
9
11
|
#
|
@@ -15,7 +17,7 @@ module DICOM
|
|
15
17
|
attr_accessor :host_ae
|
16
18
|
# The IP adress of the server.
|
17
19
|
attr_accessor :host_ip
|
18
|
-
# The maximum allowed size of network packages (in bytes).
|
20
|
+
# The maximum allowed size of network packages (in bytes).
|
19
21
|
attr_accessor :max_package_size
|
20
22
|
# The network port to be used.
|
21
23
|
attr_accessor :port
|
@@ -44,7 +46,7 @@ module DICOM
|
|
44
46
|
#
|
45
47
|
# * <tt>:ae</tt> -- String. The name of this client (application entity).
|
46
48
|
# * <tt>:host_ae</tt> -- String. The name of the server (application entity).
|
47
|
-
# * <tt>:max_package_size</tt> -- Fixnum. The maximum allowed size of network packages (in bytes).
|
49
|
+
# * <tt>:max_package_size</tt> -- Fixnum. The maximum allowed size of network packages (in bytes).
|
48
50
|
# * <tt>:timeout</tt> -- Fixnum. The maximum period the server will wait on an answer from a client before aborting the communication.
|
49
51
|
# * <tt>:verbose</tt> -- Boolean. If set to false, the DClient instance will run silently and not output warnings and error messages to the screen. Defaults to true.
|
50
52
|
#
|
@@ -73,11 +75,11 @@ module DICOM
|
|
73
75
|
@association = nil # DICOM Association status
|
74
76
|
@request_approved = nil # Status of our DICOM request
|
75
77
|
@release = nil # Status of received, valid release response
|
78
|
+
@data_elements = []
|
76
79
|
# Results from a query:
|
77
80
|
@command_results = Array.new
|
78
81
|
@data_results = Array.new
|
79
|
-
#
|
80
|
-
set_default_values
|
82
|
+
# Setup the user information used in the association request::
|
81
83
|
set_user_information_array
|
82
84
|
# Initialize the network package handler:
|
83
85
|
@link = Link.new(:ae => @ae, :host_ae => @host_ae, :max_package_size => @max_package_size, :timeout => @timeout)
|
@@ -87,34 +89,46 @@ module DICOM
|
|
87
89
|
#
|
88
90
|
def echo
|
89
91
|
# Verification SOP Class:
|
90
|
-
|
92
|
+
set_default_presentation_context(VERIFICATION_SOP)
|
91
93
|
perform_echo
|
92
94
|
end
|
93
95
|
|
94
|
-
# Queries a service class provider for images that match the specified criteria.
|
96
|
+
# Queries a service class provider for images (composite object instances) that match the specified criteria.
|
95
97
|
#
|
96
98
|
# === Parameters
|
97
99
|
#
|
98
|
-
# * <tt>
|
100
|
+
# * <tt>query_params</tt> -- A hash of query parameters.
|
99
101
|
#
|
100
102
|
# === Options
|
101
103
|
#
|
102
|
-
# * <tt>"0008,0018"</tt> --
|
103
|
-
# * <tt>"0008,0052"</tt> -- Query/Retrieve Level
|
104
|
-
# * <tt>"0020,000D"</tt> -- Study Instance UID
|
105
|
-
# * <tt>"0020,000E"</tt> -- Series Instance UID
|
104
|
+
# * <tt>"0008,0018"</tt> -- SOP Instance UID
|
106
105
|
# * <tt>"0020,0013"</tt> -- Instance Number
|
107
106
|
#
|
107
|
+
# === Notes
|
108
|
+
#
|
109
|
+
# * Caution: Calling this method without parameters will instruct your PACS to return info on ALL images in the database!
|
110
|
+
# * In addition to the above listed attributes, a number of "optional" attributes may be specified.
|
111
|
+
# * For a general list of optional object instance level attributes, refer to the DICOM standard, PS3.4 C.6.1.1.5, Table C.6-4.
|
112
|
+
#
|
108
113
|
# === Examples
|
109
114
|
#
|
110
115
|
# node.find_images("0020,000D" => "1.2.840.1145.342", "0020,000E" => "1.3.6.1.4.1.2452.6.687844")
|
111
116
|
#
|
112
|
-
def find_images(
|
117
|
+
def find_images(query_params={})
|
113
118
|
# Study Root Query/Retrieve Information Model - FIND:
|
114
|
-
|
115
|
-
#
|
116
|
-
|
117
|
-
|
119
|
+
set_default_presentation_context("1.2.840.10008.5.1.4.1.2.2.1")
|
120
|
+
# These query attributes will always be present in the dicom query:
|
121
|
+
default_query_params = {
|
122
|
+
"0008,0018" => "", # SOP Instance UID
|
123
|
+
"0008,0052" => "IMAGE", # Query/Retrieve Level: "IMAGE"
|
124
|
+
"0020,0013" => "" # Instance Number
|
125
|
+
}
|
126
|
+
# Raising an error if a non-tag query attribute is used:
|
127
|
+
query_params.keys.each do |tag|
|
128
|
+
raise ArgumentError, "The supplied tag (#{tag}) is not valid. It must be a string of the form 'GGGG,EEEE'." unless tag.is_a?(String) && tag.tag?
|
129
|
+
end
|
130
|
+
# Set up the query parameters and carry out the C-FIND:
|
131
|
+
set_data_elements(default_query_params.merge(query_params))
|
118
132
|
perform_find
|
119
133
|
return @data_results
|
120
134
|
end
|
@@ -123,7 +137,7 @@ module DICOM
|
|
123
137
|
#
|
124
138
|
# === Parameters
|
125
139
|
#
|
126
|
-
# * <tt>
|
140
|
+
# * <tt>query_params</tt> -- A hash of parameters.
|
127
141
|
#
|
128
142
|
# === Options
|
129
143
|
#
|
@@ -133,16 +147,32 @@ module DICOM
|
|
133
147
|
# * <tt>"0010,0030"</tt> -- Patient's Birth Date
|
134
148
|
# * <tt>"0010,0040"</tt> -- Patient's Sex
|
135
149
|
#
|
150
|
+
# === Notes
|
151
|
+
#
|
152
|
+
# * Caution: Calling this method without parameters will instruct your PACS to return info on ALL patients in the database!
|
153
|
+
# * In addition to the above listed attributes, a number of "optional" attributes may be specified.
|
154
|
+
# * For a general list of optional patient level attributes, refer to the DICOM standard, PS3.4 C.6.1.1.2, Table C.6-1.
|
155
|
+
#
|
136
156
|
# === Examples
|
137
157
|
#
|
138
158
|
# node.find_patients("0010,0010" => "James*")
|
139
159
|
#
|
140
|
-
def find_patients(
|
160
|
+
def find_patients(query_params={})
|
141
161
|
# Patient Root Query/Retrieve Information Model - FIND:
|
142
|
-
|
143
|
-
#
|
144
|
-
|
145
|
-
|
162
|
+
set_default_presentation_context("1.2.840.10008.5.1.4.1.2.1.1")
|
163
|
+
# Every query attribute with a value != nil (required) will be sent in the dicom query.
|
164
|
+
# The query parameters with nil-value (optional) are left out unless specified.
|
165
|
+
default_query_params = {
|
166
|
+
"0008,0052" => "PATIENT", # Query/Retrieve Level: "PATIENT"
|
167
|
+
"0010,0010" => "", # Patient's Name
|
168
|
+
"0010,0020" => "" # Patient's ID
|
169
|
+
}
|
170
|
+
# Raising an error if a non-tag query attribute is used:
|
171
|
+
query_params.keys.each do |tag|
|
172
|
+
raise ArgumentError, "The supplied tag (#{tag}) is not valid. It must be a string of the form 'GGGG,EEEE'." unless tag.is_a?(String) && tag.tag?
|
173
|
+
end
|
174
|
+
# Set up the query parameters and carry out the C-FIND:
|
175
|
+
set_data_elements(default_query_params.merge(query_params))
|
146
176
|
perform_find
|
147
177
|
return @data_results
|
148
178
|
end
|
@@ -151,27 +181,41 @@ module DICOM
|
|
151
181
|
#
|
152
182
|
# === Parameters
|
153
183
|
#
|
154
|
-
# * <tt>
|
184
|
+
# * <tt>query_params</tt> -- A hash of query parameters.
|
155
185
|
#
|
156
186
|
# === Options
|
157
187
|
#
|
158
|
-
# * <tt>"0008,0052"</tt> -- Query/Retrieve Level
|
159
188
|
# * <tt>"0008,0060"</tt> -- Modality
|
160
|
-
# * <tt>"0008,103E"</tt> -- Series Description
|
161
|
-
# * <tt>"0020,000D"</tt> -- Study Instance UID
|
162
189
|
# * <tt>"0020,000E"</tt> -- Series Instance UID
|
163
190
|
# * <tt>"0020,0011"</tt> -- Series Number
|
164
191
|
#
|
192
|
+
# === Notes
|
193
|
+
#
|
194
|
+
# * Caution: Calling this method without parameters will instruct your PACS to return info on ALL series in the database!
|
195
|
+
# * In addition to the above listed attributes, a number of "optional" attributes may be specified.
|
196
|
+
# * For a general list of optional series level attributes, refer to the DICOM standard, PS3.4 C.6.1.1.4, Table C.6-3.
|
197
|
+
#
|
165
198
|
# === Examples
|
166
199
|
#
|
167
200
|
# node.find_series("0020,000D" => "1.2.840.1145.342")
|
168
201
|
#
|
169
|
-
def find_series(
|
202
|
+
def find_series(query_params={})
|
170
203
|
# Study Root Query/Retrieve Information Model - FIND:
|
171
|
-
|
172
|
-
#
|
173
|
-
|
174
|
-
|
204
|
+
set_default_presentation_context("1.2.840.10008.5.1.4.1.2.2.1")
|
205
|
+
# Every query attribute with a value != nil (required) will be sent in the dicom query.
|
206
|
+
# The query parameters with nil-value (optional) are left out unless specified.
|
207
|
+
default_query_params = {
|
208
|
+
"0008,0052" => "SERIES", # Query/Retrieve Level: "SERIES"
|
209
|
+
"0008,0060" => "", # Modality
|
210
|
+
"0020,000E" => "", # Series Instance UID
|
211
|
+
"0020,0011" => "" # Series Number
|
212
|
+
}
|
213
|
+
# Raising an error if a non-tag query attribute is used:
|
214
|
+
query_params.keys.each do |tag|
|
215
|
+
raise ArgumentError, "The supplied tag (#{tag}) is not valid. It must be a string of the form 'GGGG,EEEE'." unless tag.is_a?(String) && tag.tag?
|
216
|
+
end
|
217
|
+
# Set up the query parameters and carry out the C-FIND:
|
218
|
+
set_data_elements(default_query_params.merge(query_params))
|
175
219
|
perform_find
|
176
220
|
return @data_results
|
177
221
|
end
|
@@ -180,34 +224,49 @@ module DICOM
|
|
180
224
|
#
|
181
225
|
# === Parameters
|
182
226
|
#
|
183
|
-
# * <tt>
|
227
|
+
# * <tt>query_params</tt> -- A hash of query parameters.
|
184
228
|
#
|
185
229
|
# === Options
|
186
230
|
#
|
187
231
|
# * <tt>"0008,0020"</tt> -- Study Date
|
188
232
|
# * <tt>"0008,0030"</tt> -- Study Time
|
189
233
|
# * <tt>"0008,0050"</tt> -- Accession Number
|
190
|
-
# * <tt>"0008,0052"</tt> -- Query/Retrieve Level
|
191
|
-
# * <tt>"0008,0090"</tt> -- Referring Physician's Name
|
192
|
-
# * <tt>"0008,1030"</tt> -- Study Description
|
193
|
-
# * <tt>"0008,1060"</tt> -- Name of Physician(s) Reading Study
|
194
234
|
# * <tt>"0010,0010"</tt> -- Patient's Name
|
195
235
|
# * <tt>"0010,0020"</tt> -- Patient ID
|
196
|
-
# * <tt>"0010,0030"</tt> -- Patient's Birth Date
|
197
|
-
# * <tt>"0010,0040"</tt> -- Patient's Sex
|
198
236
|
# * <tt>"0020,000D"</tt> -- Study Instance UID
|
199
237
|
# * <tt>"0020,0010"</tt> -- Study ID
|
200
238
|
#
|
239
|
+
# === Notes
|
240
|
+
#
|
241
|
+
# * Caution: Calling this method without parameters will instruct your PACS to return info on ALL studies in the database!
|
242
|
+
# * In addition to the above listed attributes, a number of "optional" attributes may be specified.
|
243
|
+
# * For a general list of optional study level attributes, refer to the DICOM standard, PS3.4 C.6.2.1.2, Table C.6-5.
|
244
|
+
#
|
201
245
|
# === Examples
|
202
246
|
#
|
203
247
|
# node.find_studies("0008,0020" => "20090604-", "0010,000D" => "123456789")
|
204
248
|
#
|
205
|
-
def find_studies(
|
249
|
+
def find_studies(query_params={})
|
206
250
|
# Study Root Query/Retrieve Information Model - FIND:
|
207
|
-
|
208
|
-
#
|
209
|
-
|
210
|
-
|
251
|
+
set_default_presentation_context("1.2.840.10008.5.1.4.1.2.2.1")
|
252
|
+
# Every query attribute with a value != nil (required) will be sent in the dicom query.
|
253
|
+
# The query parameters with nil-value (optional) are left out unless specified.
|
254
|
+
default_query_params = {
|
255
|
+
"0008,0020" => "", # Study Date
|
256
|
+
"0008,0030" => "", # Study Time
|
257
|
+
"0008,0050" => "", # Accession Number
|
258
|
+
"0008,0052" => "STUDY", # Query/Retrieve Level: "STUDY"
|
259
|
+
"0010,0010" => "", # Patient's Name
|
260
|
+
"0010,0020" => "", # Patient ID
|
261
|
+
"0020,000D" => "", # Study Instance UID
|
262
|
+
"0020,0010" => "" # Study ID
|
263
|
+
}
|
264
|
+
# Raising an error if a non-tag query attribute is used:
|
265
|
+
query_params.keys.each do |tag|
|
266
|
+
raise ArgumentError, "The supplied tag (#{tag}) is not valid. It must be a string of the form 'GGGG,EEEE'." unless tag.is_a?(String) && tag.tag?
|
267
|
+
end
|
268
|
+
# Set up the query parameters and carry out the C-FIND:
|
269
|
+
set_data_elements(default_query_params.merge(query_params))
|
211
270
|
perform_find
|
212
271
|
return @data_results
|
213
272
|
end
|
@@ -236,7 +295,7 @@ module DICOM
|
|
236
295
|
#
|
237
296
|
def get_image(path, options={})
|
238
297
|
# Study Root Query/Retrieve Information Model - GET:
|
239
|
-
|
298
|
+
set_default_presentation_context("1.2.840.10008.5.1.4.1.2.2.3")
|
240
299
|
# Transfer the current options to the data_elements hash:
|
241
300
|
set_command_fragment_get
|
242
301
|
# Prepare data elements for this operation:
|
@@ -269,7 +328,7 @@ module DICOM
|
|
269
328
|
#
|
270
329
|
def move_image(destination, options={})
|
271
330
|
# Study Root Query/Retrieve Information Model - MOVE:
|
272
|
-
|
331
|
+
set_default_presentation_context("1.2.840.10008.5.1.4.1.2.2.2")
|
273
332
|
# Transfer the current options to the data_elements hash:
|
274
333
|
set_command_fragment_move(destination)
|
275
334
|
# Prepare data elements for this operation:
|
@@ -301,7 +360,7 @@ module DICOM
|
|
301
360
|
#
|
302
361
|
def move_study(destination, options={})
|
303
362
|
# Study Root Query/Retrieve Information Model - MOVE:
|
304
|
-
|
363
|
+
set_default_presentation_context("1.2.840.10008.5.1.4.1.2.2.2")
|
305
364
|
# Transfer the current options to the data_elements hash:
|
306
365
|
set_command_fragment_move(destination)
|
307
366
|
# Prepare data elements for this operation:
|
@@ -322,12 +381,12 @@ module DICOM
|
|
322
381
|
#
|
323
382
|
def send(files)
|
324
383
|
# Prepare the DICOM object(s):
|
325
|
-
objects,
|
384
|
+
objects, success, message = load_files(files)
|
326
385
|
if success
|
327
386
|
# Open a DICOM link:
|
328
387
|
establish_association
|
329
|
-
if
|
330
|
-
if
|
388
|
+
if association_established?
|
389
|
+
if request_approved?
|
331
390
|
# Continue with our c-store operation, since our request was accepted.
|
332
391
|
# Handle the transmission:
|
333
392
|
perform_send(objects)
|
@@ -347,11 +406,11 @@ module DICOM
|
|
347
406
|
add_notice("TESTING CONNECTION...")
|
348
407
|
success = false
|
349
408
|
# Verification SOP Class:
|
350
|
-
|
409
|
+
set_default_presentation_context(VERIFICATION_SOP)
|
351
410
|
# Open a DICOM link:
|
352
411
|
establish_association
|
353
|
-
if
|
354
|
-
if
|
412
|
+
if association_established?
|
413
|
+
if request_approved?
|
355
414
|
success = true
|
356
415
|
end
|
357
416
|
# Close the DICOM link:
|
@@ -394,6 +453,22 @@ module DICOM
|
|
394
453
|
@notices << notice
|
395
454
|
end
|
396
455
|
|
456
|
+
# Returns an array of supported transfer syntaxes for the specified transfer syntax.
|
457
|
+
# For compressed transfer syntaxes, we currently do not support reencoding these to other syntaxes.
|
458
|
+
#
|
459
|
+
def available_transfer_syntaxes(transfer_syntax)
|
460
|
+
case transfer_syntax
|
461
|
+
when IMPLICIT_LITTLE_ENDIAN
|
462
|
+
return [IMPLICIT_LITTLE_ENDIAN, EXPLICIT_LITTLE_ENDIAN]
|
463
|
+
when EXPLICIT_LITTLE_ENDIAN
|
464
|
+
return [EXPLICIT_LITTLE_ENDIAN, IMPLICIT_LITTLE_ENDIAN]
|
465
|
+
when EXPLICIT_BIG_ENDIAN
|
466
|
+
return [EXPLICIT_BIG_ENDIAN, IMPLICIT_LITTLE_ENDIAN]
|
467
|
+
else # Compression:
|
468
|
+
return [transfer_syntax]
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
397
472
|
# Opens a TCP session with the server, and handles the association request as well as the response.
|
398
473
|
#
|
399
474
|
def establish_association
|
@@ -401,12 +476,12 @@ module DICOM
|
|
401
476
|
@association = false
|
402
477
|
@request_approved = false
|
403
478
|
# Initiate the association:
|
404
|
-
@link.build_association_request(@
|
479
|
+
@link.build_association_request(@presentation_contexts, @user_information)
|
405
480
|
@link.start_session(@host_ip, @port)
|
406
481
|
@link.transmit
|
407
482
|
info = @link.receive_multiple_transmissions.first
|
408
483
|
# Interpret the results:
|
409
|
-
if info[:valid]
|
484
|
+
if info && info[:valid]
|
410
485
|
if info[:pdu] == PDU_ASSOCIATION_ACCEPT
|
411
486
|
# Values of importance are extracted and put into instance variables:
|
412
487
|
@association = true
|
@@ -445,6 +520,14 @@ module DICOM
|
|
445
520
|
@abort = false
|
446
521
|
end
|
447
522
|
|
523
|
+
# Finds and retuns the abstract syntax that is associated with the specified context id.
|
524
|
+
#
|
525
|
+
def find_abstract_syntax(id)
|
526
|
+
@presentation_contexts.each_pair do |abstract_syntax, context_ids|
|
527
|
+
return abstract_syntax if context_ids[id]
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
448
531
|
# Loads one or more DICOM files.
|
449
532
|
# Returns an array of DObject instances, an array of unique abstract syntaxes found among the files, a status boolean and a message string.
|
450
533
|
#
|
@@ -457,28 +540,54 @@ module DICOM
|
|
457
540
|
message = ""
|
458
541
|
objects = Array.new
|
459
542
|
abstracts = Array.new
|
543
|
+
id = 1
|
544
|
+
@presentation_contexts = Hash.new
|
460
545
|
files = [files] unless files.is_a?(Array)
|
461
546
|
files.each do |file|
|
462
547
|
if file.is_a?(String)
|
463
548
|
obj = DObject.new(file, :verbose => false)
|
464
549
|
if obj.read_success
|
465
|
-
# Load the DICOM object
|
550
|
+
# Load the DICOM object:
|
466
551
|
objects << obj
|
467
|
-
abstracts << obj.value("0008,0016")
|
468
552
|
else
|
469
553
|
status = false
|
470
554
|
message = "Failed to successfully parse a DObject for the following string: #{file}"
|
471
555
|
end
|
472
556
|
elsif file.is_a?(DObject)
|
473
557
|
# Load the DICOM object and its abstract syntax:
|
474
|
-
|
475
|
-
|
558
|
+
abstracts << file.value("0008,0016")
|
559
|
+
objects << file
|
476
560
|
else
|
477
561
|
status = false
|
478
562
|
message = "Array contains invalid object #{file}."
|
479
563
|
end
|
480
564
|
end
|
481
|
-
|
565
|
+
# Extract available transfer syntaxes for the various sop classes found amongst these objects
|
566
|
+
syntaxes = Hash.new
|
567
|
+
objects.each do |obj|
|
568
|
+
sop_class = obj.value("0008,0016")
|
569
|
+
if sop_class
|
570
|
+
transfer_syntaxes = available_transfer_syntaxes(obj.transfer_syntax)
|
571
|
+
if syntaxes[sop_class]
|
572
|
+
syntaxes[sop_class] << transfer_syntaxes
|
573
|
+
else
|
574
|
+
syntaxes[sop_class] = transfer_syntaxes
|
575
|
+
end
|
576
|
+
else
|
577
|
+
status = false
|
578
|
+
message = "Missing SOP Class UID. Unable to transmit DICOM object"
|
579
|
+
end
|
580
|
+
# Extract the unique variations of SOP Class and syntaxes and construct the presentation context hash:
|
581
|
+
syntaxes.each_pair do |sop_class, ts|
|
582
|
+
selected_transfer_syntaxes = ts.flatten.uniq
|
583
|
+
@presentation_contexts[sop_class] = Hash.new
|
584
|
+
selected_transfer_syntaxes.each do |syntax|
|
585
|
+
@presentation_contexts[sop_class][id] = {:transfer_syntaxes => [syntax]}
|
586
|
+
id += 2
|
587
|
+
end
|
588
|
+
end
|
589
|
+
end
|
590
|
+
return objects, status, message
|
482
591
|
end
|
483
592
|
|
484
593
|
# Handles the communication involved in a DICOM C-ECHO.
|
@@ -488,12 +597,11 @@ module DICOM
|
|
488
597
|
def perform_echo
|
489
598
|
# Open a DICOM link:
|
490
599
|
establish_association
|
491
|
-
if
|
492
|
-
if
|
600
|
+
if association_established?
|
601
|
+
if request_approved?
|
493
602
|
# Continue with our echo, since the request was accepted.
|
494
603
|
# Set the query command elements array:
|
495
604
|
set_command_fragment_echo
|
496
|
-
presentation_context_id = @approved_syntaxes.to_a.first[1][0] # ID of first (and only) syntax in this Hash.
|
497
605
|
@link.build_command_fragment(PDU_DATA, presentation_context_id, COMMAND_LAST_FRAGMENT, @command_elements)
|
498
606
|
@link.transmit
|
499
607
|
# Listen for incoming responses and interpret them individually, until we have received the last command fragment.
|
@@ -513,12 +621,11 @@ module DICOM
|
|
513
621
|
def perform_find
|
514
622
|
# Open a DICOM link:
|
515
623
|
establish_association
|
516
|
-
if
|
517
|
-
if
|
624
|
+
if association_established?
|
625
|
+
if request_approved?
|
518
626
|
# Continue with our query, since the request was accepted.
|
519
627
|
# Set the query command elements array:
|
520
628
|
set_command_fragment_find
|
521
|
-
presentation_context_id = @approved_syntaxes.to_a.first[1][0] # ID of first (and only) syntax in this Hash.
|
522
629
|
@link.build_command_fragment(PDU_DATA, presentation_context_id, COMMAND_LAST_FRAGMENT, @command_elements)
|
523
630
|
@link.transmit
|
524
631
|
@link.build_data_fragment(@data_elements, presentation_context_id)
|
@@ -542,10 +649,9 @@ module DICOM
|
|
542
649
|
def perform_get(path)
|
543
650
|
# Open a DICOM link:
|
544
651
|
establish_association
|
545
|
-
if
|
546
|
-
if
|
652
|
+
if association_established?
|
653
|
+
if request_approved?
|
547
654
|
# Continue with our operation, since the request was accepted.
|
548
|
-
presentation_context_id = @approved_syntaxes.to_a.first[1][0] # ID of first (and only) syntax in this Hash.
|
549
655
|
@link.build_command_fragment(PDU_DATA, presentation_context_id, COMMAND_LAST_FRAGMENT, @command_elements)
|
550
656
|
@link.transmit
|
551
657
|
@link.build_data_fragment(@data_elements, presentation_context_id)
|
@@ -569,10 +675,9 @@ module DICOM
|
|
569
675
|
def perform_move
|
570
676
|
# Open a DICOM link:
|
571
677
|
establish_association
|
572
|
-
if
|
573
|
-
if
|
678
|
+
if association_established?
|
679
|
+
if request_approved?
|
574
680
|
# Continue with our operation, since the request was accepted.
|
575
|
-
presentation_context_id = @approved_syntaxes.to_a.first[1][0] # ID of first (and only) syntax in this Hash.
|
576
681
|
@link.build_command_fragment(PDU_DATA, presentation_context_id, COMMAND_LAST_FRAGMENT, @command_elements)
|
577
682
|
@link.transmit
|
578
683
|
@link.build_data_fragment(@data_elements, presentation_context_id)
|
@@ -593,22 +698,24 @@ module DICOM
|
|
593
698
|
def perform_send(objects)
|
594
699
|
objects.each_with_index do |obj, index|
|
595
700
|
# Gather necessary information from the object (SOP Class & Instance UID):
|
596
|
-
|
597
|
-
|
598
|
-
if
|
599
|
-
# Only send the image if its
|
600
|
-
if @approved_syntaxes[
|
701
|
+
sop_class = obj.value("0008,0016")
|
702
|
+
sop_instance = obj.value("0008,0018")
|
703
|
+
if sop_class and sop_instance
|
704
|
+
# Only send the image if its sop_class has been accepted by the receiver:
|
705
|
+
if @approved_syntaxes[sop_class]
|
601
706
|
# Set the command array to be used:
|
602
707
|
message_id = index + 1
|
603
|
-
set_command_fragment_store(
|
708
|
+
set_command_fragment_store(sop_class, sop_instance, message_id)
|
604
709
|
# Find context id and transfer syntax:
|
605
|
-
presentation_context_id = @approved_syntaxes[
|
606
|
-
selected_transfer_syntax = @approved_syntaxes[
|
710
|
+
presentation_context_id = @approved_syntaxes[sop_class][0]
|
711
|
+
selected_transfer_syntax = @approved_syntaxes[sop_class][1]
|
607
712
|
# Encode our DICOM object to a binary string which is split up in pieces, sufficiently small to fit within the specified maximum pdu length:
|
608
713
|
# Set the transfer syntax of the DICOM object equal to the one accepted by the SCP:
|
609
714
|
obj.transfer_syntax = selected_transfer_syntax
|
715
|
+
# Remove the Meta group, since it doesn't belong in a DICOM file transfer:
|
716
|
+
obj.remove_group(META_GROUP)
|
610
717
|
max_header_length = 14
|
611
|
-
data_packages = obj.encode_segments(@max_pdu_length - max_header_length)
|
718
|
+
data_packages = obj.encode_segments(@max_pdu_length - max_header_length, selected_transfer_syntax)
|
612
719
|
@link.build_command_fragment(PDU_DATA, presentation_context_id, COMMAND_LAST_FRAGMENT, @command_elements)
|
613
720
|
@link.transmit
|
614
721
|
# Transmit all but the last data strings:
|
@@ -643,13 +750,14 @@ module DICOM
|
|
643
750
|
rejected = Hash.new
|
644
751
|
# Reset the presentation context instance variable:
|
645
752
|
@link.presentation_contexts = Hash.new
|
753
|
+
accepted_pc = 0
|
646
754
|
presentation_contexts.each do |pc|
|
647
755
|
# Determine what abstract syntax this particular presentation context's id corresponds to:
|
648
756
|
id = pc[:presentation_context_id]
|
649
757
|
raise "Error! Even presentation context ID received in the association response. This is not allowed according to the DICOM standard!" if id[0] == 0 # If even number.
|
650
|
-
|
651
|
-
abstract_syntax = @abstract_syntaxes[index]
|
758
|
+
abstract_syntax = find_abstract_syntax(id)
|
652
759
|
if pc[:result] == 0
|
760
|
+
accepted_pc += 1
|
653
761
|
@approved_syntaxes[abstract_syntax] = [id, pc[:transfer_syntax]]
|
654
762
|
@link.presentation_contexts[id] = pc[:transfer_syntax]
|
655
763
|
else
|
@@ -658,16 +766,17 @@ module DICOM
|
|
658
766
|
end
|
659
767
|
if rejected.length == 0
|
660
768
|
@request_approved = true
|
661
|
-
if @approved_syntaxes.length == 1
|
769
|
+
if @approved_syntaxes.length == 1 and presentation_contexts.length == 1
|
662
770
|
add_notice("The presentation context was accepted by host #{@host_ae}.")
|
663
771
|
else
|
664
|
-
add_notice("All #{
|
772
|
+
add_notice("All #{presentation_contexts.length} presentation contexts were accepted by host #{@host_ae} (#{@host_ip}).")
|
665
773
|
end
|
666
774
|
else
|
667
|
-
|
775
|
+
# We still consider the request 'approved' if at least one context were accepted:
|
776
|
+
@request_approved = true if @approved_syntaxes.length > 0
|
668
777
|
add_error("One or more of your presentation contexts were denied by host #{@host_ae}!")
|
669
|
-
@approved_syntaxes.
|
670
|
-
rejected.
|
778
|
+
@approved_syntaxes.each_pair {|key, value| add_error("APPROVED: #{LIBRARY.get_syntax_description(key)} (#{LIBRARY.get_syntax_description(value[1])})")}
|
779
|
+
rejected.each_pair {|key, value| add_error("REJECTED: #{LIBRARY.get_syntax_description(key)} (#{LIBRARY.get_syntax_description(value[1])})")}
|
671
780
|
end
|
672
781
|
end
|
673
782
|
|
@@ -695,7 +804,7 @@ module DICOM
|
|
695
804
|
#
|
696
805
|
def set_command_fragment_echo
|
697
806
|
@command_elements = [
|
698
|
-
["0000,0002", "UI", @
|
807
|
+
["0000,0002", "UI", @presentation_contexts.keys.first], # Affected SOP Class UID
|
699
808
|
["0000,0100", "US", C_ECHO_RQ],
|
700
809
|
["0000,0110", "US", DEFAULT_MESSAGE_ID],
|
701
810
|
["0000,0800", "US", NO_DATA_SET_PRESENT]
|
@@ -710,7 +819,7 @@ module DICOM
|
|
710
819
|
#
|
711
820
|
def set_command_fragment_find
|
712
821
|
@command_elements = [
|
713
|
-
["0000,0002", "UI", @
|
822
|
+
["0000,0002", "UI", @presentation_contexts.keys.first], # Affected SOP Class UID
|
714
823
|
["0000,0100", "US", C_FIND_RQ],
|
715
824
|
["0000,0110", "US", DEFAULT_MESSAGE_ID],
|
716
825
|
["0000,0700", "US", 0], # Priority: 0: medium
|
@@ -722,7 +831,7 @@ module DICOM
|
|
722
831
|
#
|
723
832
|
def set_command_fragment_get
|
724
833
|
@command_elements = [
|
725
|
-
["0000,0002", "UI", @
|
834
|
+
["0000,0002", "UI", @presentation_contexts.keys.first], # Affected SOP Class UID
|
726
835
|
["0000,0100", "US", C_GET_RQ],
|
727
836
|
["0000,0600", "AE", @ae], # Destination is ourselves
|
728
837
|
["0000,0700", "US", 0], # Priority: 0: medium
|
@@ -734,7 +843,7 @@ module DICOM
|
|
734
843
|
#
|
735
844
|
def set_command_fragment_move(destination)
|
736
845
|
@command_elements = [
|
737
|
-
["0000,0002", "UI", @
|
846
|
+
["0000,0002", "UI", @presentation_contexts.keys.first], # Affected SOP Class UID
|
738
847
|
["0000,0100", "US", C_MOVE_RQ],
|
739
848
|
["0000,0110", "US", DEFAULT_MESSAGE_ID],
|
740
849
|
["0000,0600", "AE", destination],
|
@@ -756,63 +865,6 @@ module DICOM
|
|
756
865
|
]
|
757
866
|
end
|
758
867
|
|
759
|
-
# Sets the data elements used in a query for the images of a particular series.
|
760
|
-
#
|
761
|
-
def set_data_fragment_find_images
|
762
|
-
@data_elements = [
|
763
|
-
["0008,0018", ""], # SOP Instance UID
|
764
|
-
["0008,0052", "IMAGE"], # Query/Retrieve Level: "IMAGE"
|
765
|
-
["0020,000D", ""], # Study Instance UID
|
766
|
-
["0020,000E", ""], # Series Instance UID
|
767
|
-
["0020,0013", ""] # Instance Number
|
768
|
-
]
|
769
|
-
end
|
770
|
-
|
771
|
-
# Sets the data elements used in a query for patients.
|
772
|
-
#
|
773
|
-
def set_data_fragment_find_patients
|
774
|
-
@data_elements = [
|
775
|
-
["0008,0052", "PATIENT"], # Query/Retrieve Level: "PATIENT"
|
776
|
-
["0010,0010", ""], # Patient's Name
|
777
|
-
["0010,0020", ""], # Patient ID
|
778
|
-
["0010,0030", ""], # Patient's Birth Date
|
779
|
-
["0010,0040", ""] # Patient's Sex
|
780
|
-
]
|
781
|
-
end
|
782
|
-
|
783
|
-
# Sets the data elements used in a query for the series of a particular study.
|
784
|
-
#
|
785
|
-
def set_data_fragment_find_series
|
786
|
-
@data_elements = [
|
787
|
-
["0008,0052", "SERIES"], # Query/Retrieve Level: "SERIES"
|
788
|
-
["0008,0060", ""], # Modality
|
789
|
-
["0008,103E", ""], # Series Description
|
790
|
-
["0020,000D", ""], # Study Instance UID
|
791
|
-
["0020,000E", ""], # Series Instance UID
|
792
|
-
["0020,0011", ""] # Series Number
|
793
|
-
]
|
794
|
-
end
|
795
|
-
|
796
|
-
# Sets the data elements used in a query for studies.
|
797
|
-
#
|
798
|
-
def set_data_fragment_find_studies
|
799
|
-
@data_elements = [
|
800
|
-
["0008,0020", ""], # Study Date
|
801
|
-
["0008,0030", ""], # Study Time
|
802
|
-
["0008,0050", ""], # Accession Number
|
803
|
-
["0008,0052", "STUDY"], # Query/Retrieve Level: "STUDY"
|
804
|
-
["0008,0090", ""], # Referring Physician's Name
|
805
|
-
["0008,1030", ""], # Study Description
|
806
|
-
["0008,1060", ""], # Name of Physician(s) Reading Study
|
807
|
-
["0010,0010", ""], # Patient's Name
|
808
|
-
["0010,0020", ""], # Patient ID
|
809
|
-
["0010,0030", ""], # Patient's Birth Date
|
810
|
-
["0010,0040", ""], # Patient's Sex
|
811
|
-
["0020,000D", ""], # Study Instance UID
|
812
|
-
["0020,0010", ""] # Study ID
|
813
|
-
]
|
814
|
-
end
|
815
|
-
|
816
868
|
# Sets the data elements used for an image C-GET-RQ.
|
817
869
|
#
|
818
870
|
def set_data_fragment_get_image
|
@@ -845,11 +897,11 @@ module DICOM
|
|
845
897
|
]
|
846
898
|
end
|
847
899
|
|
848
|
-
# Transfers the user
|
900
|
+
# Transfers the user-specified options to the @data_elements instance array.
|
849
901
|
#
|
850
902
|
# === Restrictions
|
851
903
|
#
|
852
|
-
# * Only tag & value pairs for tags which are predefined for the specific
|
904
|
+
# * Only tag & value pairs for tags which are predefined for the specific request type will be stored!
|
853
905
|
#
|
854
906
|
def set_data_options(options)
|
855
907
|
options.each_pair do |key, value|
|
@@ -861,13 +913,15 @@ module DICOM
|
|
861
913
|
end
|
862
914
|
end
|
863
915
|
|
864
|
-
#
|
916
|
+
# Creates the presentation context used for the non-file-transmission association requests..
|
865
917
|
#
|
866
|
-
def
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
918
|
+
def set_default_presentation_context(abstract_syntax)
|
919
|
+
raise ArgumentError, "Expected String, got #{abstract_syntax.class}" unless abstract_syntax.is_a?(String)
|
920
|
+
id = 1
|
921
|
+
transfer_syntaxes = [IMPLICIT_LITTLE_ENDIAN, EXPLICIT_LITTLE_ENDIAN, EXPLICIT_BIG_ENDIAN]
|
922
|
+
item = {:transfer_syntaxes => transfer_syntaxes}
|
923
|
+
pc = {id => item}
|
924
|
+
@presentation_contexts = {abstract_syntax => pc}
|
871
925
|
end
|
872
926
|
|
873
927
|
# Sets the @user_information items instance array.
|
@@ -884,5 +938,24 @@ module DICOM
|
|
884
938
|
]
|
885
939
|
end
|
886
940
|
|
941
|
+
def association_established?
|
942
|
+
@association == true
|
943
|
+
end
|
944
|
+
|
945
|
+
def request_approved?
|
946
|
+
@request_approved == true
|
947
|
+
end
|
948
|
+
|
949
|
+
def presentation_context_id
|
950
|
+
@approved_syntaxes.to_a.first[1][0] # ID of first (and only) syntax in this Hash.
|
951
|
+
end
|
952
|
+
|
953
|
+
def set_data_elements(options)
|
954
|
+
@data_elements = []
|
955
|
+
options.keys.sort.each do |tag|
|
956
|
+
@data_elements << [ tag, options[tag] ] unless options[tag].nil?
|
957
|
+
end
|
958
|
+
end
|
959
|
+
|
887
960
|
end
|
888
961
|
end
|