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
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
|