dicom 0.7 → 0.8

Sign up to get free protection for your applications and to get access to all the features.
data/lib/dicom/DClient.rb DELETED
@@ -1,584 +0,0 @@
1
- # Copyright 2009-2010 Christoffer Lervag
2
-
3
- module DICOM
4
-
5
- # This class contains code for handling the client side of DICOM TCP/IP network communication.
6
- class DClient
7
-
8
- attr_accessor :ae, :host_ae, :host_ip, :max_package_size, :port, :timeout, :verbose
9
- attr_reader :command_results, :data_results, :errors, :notices
10
-
11
- # Initialize the instance with a host adress and a port number.
12
- def initialize(host_ip, port, options={})
13
- require 'socket'
14
- # Required parameters:
15
- @host_ip = host_ip
16
- @port = port
17
- # Optional parameters (and default values):
18
- @ae = options[:ae] || "RUBY_DICOM"
19
- @host_ae = options[:host_ae] || "DEFAULT"
20
- @max_package_size = options[:max_package_size] || 32768 # 16384
21
- @timeout = options[:timeout] || 10 # seconds
22
- @min_length = 12 # minimum number of bytes to expect in an incoming transmission
23
- @verbose = options[:verbose]
24
- @verbose = true if @verbose == nil # Default verbosity is 'on'.
25
- # Other instance variables:
26
- @errors = Array.new # errors and warnings are put in this array
27
- @notices = Array.new # information on successful transmissions are put in this array
28
- # Variables used for monitoring state of transmission:
29
- @connection = nil # TCP connection status
30
- @association = nil # DICOM Association status
31
- @request_approved = nil # Status of our DICOM request
32
- @release = nil # Status of received, valid release response
33
- # Results from a query:
34
- @command_results = Array.new
35
- @data_results = Array.new
36
- # Set default values like transfer syntax, user information, endianness:
37
- set_default_values
38
- set_user_information_array
39
- # Initialize the network package handler:
40
- @link = Link.new(:ae => @ae, :host_ae => @host_ae, :max_package_size => @max_package_size, :timeout => @timeout)
41
- end
42
-
43
-
44
- # Query a service class provider for images that match the specified criteria.
45
- # Example: find_images("0010,0020" => "123456789", "0020,000D" => "1.2.840.1145.342", "0020,000E" => "1.3.6.1.4.1.2452.6.687844") # (Patient ID, Study Instance UID & Series Instance UID)
46
- def find_images(options={})
47
- # Study Root Query/Retrieve Information Model - FIND:
48
- @abstract_syntax = "1.2.840.10008.5.1.4.1.2.2.1"
49
- # Prepare data elements for this operation:
50
- set_data_fragment_find_images
51
- set_data_options(options)
52
- perform_find
53
- return @data_results
54
- end
55
-
56
-
57
- # Query a service class provider for patients that match the specified criteria.
58
- # Example: find_patients("0010,0010" => "James*") # (Patient's Name)
59
- def find_patients(options={})
60
- # Patient Root Query/Retrieve Information Model - FIND:
61
- @abstract_syntax = "1.2.840.10008.5.1.4.1.2.1.1"
62
- # Prepare data elements for this operation:
63
- set_data_fragment_find_patients
64
- set_data_options(options)
65
- perform_find
66
- return @data_results
67
- end
68
-
69
-
70
- # Query a service class provider for series that match the specified criteria.
71
- # Example: find_series("0010,0020" => "123456789", "0020,000D" => "1.2.840.1145.342") # (Patient ID & Study Instance UID)
72
- def find_series(options={})
73
- # Study Root Query/Retrieve Information Model - FIND:
74
- @abstract_syntax = "1.2.840.10008.5.1.4.1.2.2.1"
75
- # Prepare data elements for this operation:
76
- set_data_fragment_find_series
77
- set_data_options(options)
78
- perform_find
79
- return @data_results
80
- end
81
-
82
-
83
- # Query a service class provider for studies that match the specified criteria.
84
- # Example: find_studies("0008,0020" => "20090604-", "0010,000D" => "123456789") # (Study Date & Patient ID)
85
- def find_studies(options={})
86
- # Study Root Query/Retrieve Information Model - FIND:
87
- @abstract_syntax = "1.2.840.10008.5.1.4.1.2.2.1"
88
- # Prepare data elements for this operation:
89
- set_data_fragment_find_studies
90
- set_data_options(options)
91
- perform_find
92
- return @data_results
93
- end
94
-
95
-
96
- # Retrieve a dicom file from a service class provider (SCP/PACS).
97
- # Example: get_image("c:/dicom/", "0008,0018" => sop_uid, "0020,000D" => study_uid, "0020,000E" => series_uid)
98
- def get_image(path, options={})
99
- # Study Root Query/Retrieve Information Model - GET:
100
- @abstract_syntax = "1.2.840.10008.5.1.4.1.2.2.3"
101
- # Transfer the current options to the data_elements hash:
102
- set_command_fragment_get
103
- # Prepare data elements for this operation:
104
- set_data_fragment_get_image
105
- set_data_options(options)
106
- perform_get(path)
107
- end
108
-
109
-
110
- # Move an image to a dicom node other than yourself.
111
- # Example: move_image("MYDICOM", "0008,0018" => sop_uid, "0020,000D" => study_uid, "0020,000E" => series_uid)
112
- def move_image(destination, options={})
113
- # Study Root Query/Retrieve Information Model - MOVE:
114
- @abstract_syntax = "1.2.840.10008.5.1.4.1.2.2.2"
115
- # Transfer the current options to the data_elements hash:
116
- set_command_fragment_move(destination)
117
- # Prepare data elements for this operation:
118
- set_data_fragment_move_image
119
- set_data_options(options)
120
- perform_move
121
- end
122
-
123
-
124
- # Move an entire study to a dicom node other than yourself.
125
- # Example: move_study("MYDICOM", "0010,0020" => pat_id, "0020,000D" => study_uid)
126
- def move_study(destination, options={})
127
- # Study Root Query/Retrieve Information Model - MOVE:
128
- @abstract_syntax = "1.2.840.10008.5.1.4.1.2.2.2"
129
- # Transfer the current options to the data_elements hash:
130
- set_command_fragment_move(destination)
131
- # Prepare data elements for this operation:
132
- set_data_fragment_move_study
133
- set_data_options(options)
134
- perform_move
135
- end
136
-
137
-
138
- # Send a DICOM file to a service class provider (SCP/PACS).
139
- def send(file_path)
140
- # Load the DICOM file from the specified path:
141
- obj = DObject.new(file_path, :verbose => false)
142
- if obj.read_success
143
- # Get the SOP Class UID (abstract syntax) from the DICOM obj:
144
- @abstract_syntax = obj.get_value("0008,0016")
145
- # Get the Transfer Syntax UID from the DICOM obj,
146
- # and if not available, set to default: Implicit, Little endian
147
- @transfer_syntax = [obj.get_value("0002,0010")] || ["1.2.840.10008.1.2"]
148
- # Open a DICOM link:
149
- establish_association
150
- if @association
151
- if @request_approved
152
- # Continue with our c-store operation, since our request was accepted.
153
- # Prepare the DICOM object for transmission:
154
- obj.encode_segments(@max_pdu_length - 14)
155
- # Handle the transmission:
156
- perform_send(obj)
157
- end
158
- end
159
- else
160
- # Failed to read DICOM file. Can not transmit this file.
161
- add_error("Error: The supplied file was not recognised as a valid DICOM file. File NOT transmitted. (file: #{file_path}")
162
- end
163
- # Close the DICOM link:
164
- establish_release
165
- end
166
-
167
-
168
- # Tests the connection to the specified host by trying to negotiate an association, then releasing it.
169
- def test
170
- add_notice("TESTING CONNECTION...")
171
- success = false
172
- # Verification SOP Class:
173
- @abstract_syntax = "1.2.840.10008.1.1"
174
- # Open a DICOM link:
175
- establish_association
176
- if @association
177
- if @request_approved
178
- success = true
179
- end
180
- # Close the DICOM link:
181
- establish_release
182
- end
183
- if success
184
- add_notice("TEST SUCCSESFUL!")
185
- else
186
- add_error("TEST FAILED!")
187
- end
188
- return success
189
- end
190
-
191
-
192
- # Following methods are private:
193
- private
194
-
195
-
196
- # Adds a warning or error message to the instance array holding messages,
197
- # and if verbose variable is true, prints the message as well.
198
- def add_error(error)
199
- puts error if @verbose
200
- @errors << error
201
- end
202
-
203
-
204
- # Adds a notice (information regarding progress or successful communications) to the instance array,
205
- # and if verbosity is set for these kinds of messages, prints it to the screen as well.
206
- def add_notice(notice)
207
- puts notice if @verbose
208
- @notices << notice
209
- end
210
-
211
-
212
- # Open a TCP session with a specified server, and handle the association request along with its response.
213
- def establish_association
214
- # Reset some variables:
215
- @association = false
216
- @request_approved = false
217
- # Initiate the association:
218
- @link.build_association_request(@application_context_uid, @abstract_syntax, @transfer_syntax, @user_information)
219
- @connection = TCPSocket.new(@host_ip, @port)
220
- @link.transmit(@connection)
221
- info = @link.receive_multiple_transmissions(@connection).first
222
- # Interpret the results:
223
- if info[:valid]
224
- if info[:pdu] == "02"
225
- # Values of importance are extracted and put into instance variables:
226
- @association = true
227
- @max_pdu_length = info[:max_pdu_length]
228
- @presentation_context_id = info[:presentation_context_id]
229
- add_notice("Association successfully negotiated with host #{host_ae} (#{host_ip}).")
230
- else
231
- add_error("Association was denied from host #{host_ae} (#{host_ip})!")
232
- end
233
- if info[:result] == 0
234
- @request_approved = true
235
- add_notice("Your request was accepted by host #{host_ae} (#{host_ip}).")
236
- else
237
- add_error("Your request was denied by host #{host_ae} (#{host_ip})!")
238
- end
239
- end
240
- end
241
-
242
-
243
- # Handle a release request and its response, as well as closing the TCP connection.
244
- def establish_release
245
- @release = false
246
- if @abort
247
- @connection.close unless @connection.closed?
248
- add_notice("Association has been closed. (#{host_ae}, #{host_ip})")
249
- else
250
- unless @connection.closed?
251
- @link.build_release_request
252
- @link.transmit(@connection)
253
- info = @link.receive_single_transmission(@connection).first
254
- @connection.close
255
- if info[:pdu] == "06"
256
- add_notice("Association released properly from host #{host_ae} (#{host_ip}).")
257
- else
258
- add_error("Association was NOT released properly for some reason from host #{host_ae} (#{host_ip})!")
259
- end
260
- else
261
- add_error("Connection was closed by the host (for some unknown reason) before the association could be released properly.")
262
- end
263
- end
264
- @abort = false
265
- end
266
-
267
-
268
- # Handle the communication involved in DICOM query (C-FIND).
269
- # Build the necessary strings and send the command and data element that makes up the query.
270
- # Listens for and interpretes the incoming query responses.
271
- def perform_find
272
- # Open a DICOM link:
273
- establish_association
274
- if @association
275
- if @request_approved
276
- # Continue with our query, since the request was accepted.
277
- # Set the query command elements array:
278
- set_command_fragment_find
279
- pdu="04"
280
- #context = "01"
281
- flags = "03"
282
- @link.build_command_fragment(pdu, @presentation_context_id, flags, @command_elements)
283
- @link.transmit(@connection)
284
- @link.build_data_fragment(@data_elements)
285
- @link.transmit(@connection)
286
- # A query response will typically be sent in multiple, separate packets.
287
- # Listen for incoming responses and interpret them individually, until we have received the last command fragment.
288
- segments = @link.receive_multiple_transmissions(@connection)
289
- process_returned_data(segments)
290
- end
291
- # Close the DICOM link:
292
- establish_release
293
- end
294
- end
295
-
296
-
297
- # Build and send command & data fragment, then receive the incoming file data.
298
- def perform_get(path)
299
- # Open a DICOM link:
300
- establish_association
301
- if @association
302
- if @request_approved
303
- # Continue with our operation, since the request was accepted.
304
- pdu="04"
305
- flags = "03"
306
- @link.build_command_fragment(pdu, @presentation_context_id, flags, @command_elements)
307
- @link.transmit(@connection)
308
- @link.build_data_fragment(@data_elements) # (uses flag = 02)
309
- @link.transmit(@connection)
310
- # Listen for incoming file data:
311
- success = @link.handle_incoming_data(@connection, path)
312
- if success
313
- # Send confirmation response:
314
- @link.handle_response(@connection)
315
- end
316
- end
317
- # Close the DICOM link:
318
- establish_release
319
- end
320
- end
321
-
322
-
323
- # Handle the communication involved in DICOM move request.
324
- def perform_move
325
- # Open a DICOM link:
326
- establish_association
327
- if @association
328
- if @request_approved
329
- # Continue with our operation, since the request was accepted.
330
- pdu="04"
331
- flags = "03"
332
- @link.build_command_fragment(pdu, @presentation_context_id, flags, @command_elements)
333
- @link.transmit(@connection)
334
- flags = "02"
335
- @link.build_data_fragment(@data_elements)
336
- @link.transmit(@connection)
337
- # Receive confirmation response:
338
- segments = @link.receive_single_transmission(@connection)
339
- process_returned_data(segments)
340
- end
341
- # Close the DICOM link:
342
- establish_release
343
- end
344
- end
345
-
346
-
347
- # Builds and sends the command fragment, then builds and sends the data fragments that
348
- # conveys the information from the original DICOM file.
349
- def perform_send(obj)
350
- # Set the command array to be used:
351
- sop_uid = obj.get_value("SOP Instance UID") # 0008,0018
352
- if sop_uid
353
- set_command_fragment_store(sop_uid)
354
- pdu_type = "04"
355
- flags = "03"
356
- @link.build_command_fragment(pdu_type, @presentation_context_id, flags, @command_elements)
357
- @link.transmit(@connection)
358
- # Transmit all but the last segments:
359
- flags = "00"
360
- (0..obj.segments.length-2).each do |i|
361
- @link.build_storage_fragment(pdu_type, @presentation_context_id, flags, obj.segments[i])
362
- @link.transmit(@connection)
363
- end
364
- # Transmit the last segment:
365
- flags = "02"
366
- @link.build_storage_fragment(pdu_type, @presentation_context_id, flags, obj.segments.last)
367
- @link.transmit(@connection)
368
- # Receive confirmation response:
369
- segments = @link.receive_single_transmission(@connection)
370
- process_returned_data(segments)
371
- else
372
- add_error("Error: Unable to extract SOP Instance UID for the given DICOM file. File will not be sent to its destination.")
373
- end
374
- end
375
-
376
-
377
- # Process the data that was returned from the interaction with the SCP and make it available to the user.
378
- def process_returned_data(segments)
379
- # Reset command results arrays:
380
- @command_results = Array.new
381
- @data_results = Array.new
382
- # Try to extract data:
383
- segments.each do |info|
384
- if info[:valid]
385
- # Determine if it is command or data:
386
- if info[:presentation_context_flag] == "03"
387
- # Command (last fragment):
388
- @command_results << info[:results]
389
- elsif info[:presentation_context_flag] == "02"
390
- # Data (last fragment)
391
- @data_results << info[:results]
392
- end
393
- end
394
- end
395
- end
396
-
397
-
398
- # Reset the values of a array.
399
- # It is assumed the arrays elements are an array in itself, where element[1]
400
- # will be reset to the string value "".
401
- def reset(array)
402
- array.each do |element|
403
- element[1] = ""
404
- end
405
- end
406
-
407
-
408
- # Set command elements used in a C-GET-RQ:
409
- def set_command_fragment_get
410
- @command_elements = [
411
- ["0000,0002", "UI", @abstract_syntax], # Affected SOP Class UID
412
- ["0000,0100", "US", 16], # Command Field: 16 (C-GET-RQ)
413
- ["0000,0600", "AE", @ae], # Destination is ourselves
414
- ["0000,0700", "US", 0], # Priority: 0: medium
415
- ["0000,0800", "US", 1] # Data Set Type: 1
416
- ]
417
- end
418
-
419
-
420
- # Command elements used in a C-FIND-RQ.
421
- # This seems to be the same, regardless of what we want to query.
422
- def set_command_fragment_find
423
- @command_elements = [
424
- ["0000,0002", "UI", @abstract_syntax], # Affected SOP Class UID
425
- ["0000,0100", "US", 32], # Command Field: 32 (C-FIND-RQ)
426
- ["0000,0110", "US", 1], # Message ID: 1
427
- ["0000,0700", "US", 0], # Priority: 0: medium
428
- ["0000,0800", "US", 1] # Data Set Type: 1
429
- ]
430
- end
431
-
432
-
433
- # Set command elements used in a C-MOVE-RQ:
434
- def set_command_fragment_move(destination)
435
- @command_elements = [
436
- ["0000,0002", "UI", @abstract_syntax], # Affected SOP Class UID
437
- ["0000,0100", "US", 33], # Command Field: 33 (C-MOVE-RQ)
438
- ["0000,0110", "US", 1], # Message ID: 1
439
- ["0000,0600", "AE", destination], # Move destination
440
- ["0000,0700", "US", 0], # Priority: 0: medium
441
- ["0000,0800", "US", 1] # Data Set Type: 1
442
- ]
443
- end
444
-
445
-
446
- # Command elements used in a p-data c-store-rq query command:
447
- def set_command_fragment_store(sop_uid)
448
- @command_elements = [
449
- ["0000,0002", "UI", @abstract_syntax], # Affected SOP Class UID
450
- ["0000,0100", "US", 1], # Command Field: 1 (C-STORE-RQ)
451
- ["0000,0110", "US", 1], # Message ID: 1
452
- ["0000,0700", "US", 0], # Priority: 0: medium
453
- ["0000,0800", "US", 1], # Data Set Type: 1
454
- ["0000,1000", "UI", sop_uid] # Affected SOP Instance UID
455
- ]
456
- end
457
-
458
-
459
- # Data elements used in a query for the images of a particular series:
460
- def set_data_fragment_find_images
461
- @data_elements = [
462
- ["0008,0018", ""], # SOP Instance UID
463
- ["0008,0052", "IMAGE"], # Query/Retrieve Level: "IMAGE"
464
- ["0020,000D", ""], # Study Instance UID
465
- ["0020,000E", ""], # Series Instance UID
466
- ["0020,0013", ""] # Instance Number
467
- ]
468
- end
469
-
470
-
471
- # Data elements used in a query for patients:
472
- def set_data_fragment_find_patients
473
- @data_elements = [
474
- ["0008,0052", "PATIENT"], # Query/Retrieve Level: "PATIENT"
475
- ["0010,0010", ""], # Patient's Name
476
- ["0010,0020", ""], # Patient ID
477
- ["0010,0030", ""], # Patient's Birth Date
478
- ["0010,0040", ""] # Patient's Sex
479
- ]
480
- end
481
-
482
-
483
- # Data elements used in a query for the series of a particular study:
484
- def set_data_fragment_find_series
485
- @data_elements = [
486
- ["0008,0052", "SERIES"], # Query/Retrieve Level: "SERIES"
487
- ["0008,0060", ""], # Modality
488
- ["0008,103E", ""], # Series Description
489
- ["0020,000D", ""], # Study Instance UID
490
- ["0020,000E", ""], # Series Instance UID
491
- ["0020,0011", ""] # Series Number
492
- ]
493
- end
494
-
495
-
496
- # Data elements used in a query for studies:
497
- def set_data_fragment_find_studies
498
- @data_elements = [
499
- ["0008,0020", ""], # Study Date
500
- ["0008,0030", ""], # Study Time
501
- ["0008,0050", ""], # Accession Number
502
- ["0008,0052", "STUDY"], # Query/Retrieve Level: "STUDY"
503
- ["0008,0090", ""], # Referring Physician's Name
504
- ["0008,1030", ""], # Study Description
505
- ["0008,1060", ""], # Name of Physician(s) Reading Study
506
- ["0010,0010", ""], # Patient's Name
507
- ["0010,0020", ""], # Patient ID
508
- ["0010,0030", ""], # Patient's Birth Date
509
- ["0010,0040", ""], # Patient's Sex
510
- ["0020,000D", ""], # Study Instance UID
511
- ["0020,0010", ""] # Study ID
512
- ]
513
- end
514
-
515
-
516
- # Set data elements used for an image C-GET-RQ:
517
- def set_data_fragment_get_image
518
- @data_elements = [
519
- ["0008,0018", ""], # SOP Instance UID
520
- ["0008,0052", "IMAGE"], # Query/Retrieve Level: "IMAGE"
521
- ["0020,000D", ""], # Study Instance UID
522
- ["0020,000E", ""] # Series Instance UID
523
- ]
524
- end
525
-
526
-
527
- # Set data elements used for an image C-MOVE-RQ:
528
- def set_data_fragment_move_image
529
- @data_elements = [
530
- ["0008,0018", ""], # SOP Instance UID
531
- ["0008,0052", "IMAGE"], # Query/Retrieve Level: "IMAGE"
532
- ["0020,000D", ""], # Study Instance UID
533
- ["0020,000E", ""] # Series Instance UID
534
- ]
535
- end
536
-
537
-
538
- # Set data elements used in a study C-MOVE-RQ:
539
- def set_data_fragment_move_study
540
- @data_elements = [
541
- ["0008,0052", "STUDY"], # Query/Retrieve Level: "STUDY"
542
- ["0010,0020", ""], # Patient ID
543
- ["0020,000D", ""] # Study Instance UID
544
- ]
545
- end
546
-
547
-
548
- # Transfer the user query options to the data elements array.
549
- # NB: Only tags which are predefined for the specific query type will be updated
550
- # (no new tags are allowed stored among the data elements)
551
- def set_data_options(options)
552
- options.each_pair do |key, value|
553
- tags = @data_elements.transpose[0]
554
- i = tags.index(key)
555
- if i
556
- @data_elements[i][1] = value
557
- end
558
- end
559
- end
560
-
561
-
562
- # Set default values for accepted transfer syntaxes:
563
- def set_default_values
564
- # DICOM Application Context Name (unknown if this will vary or is always the same):
565
- @application_context_uid = "1.2.840.10008.3.1.1.1"
566
- # Transfer syntax (preferred syntax appearing first)
567
- @transfer_syntax = ["1.2.840.10008.1.2.1", # Explicit VR Little Endian
568
- "1.2.840.10008.1.2.2", # Explicit VR Big Endian
569
- "1.2.840.10008.1.2" # Implicit VR Little Endian
570
- ]
571
- end
572
-
573
-
574
- # Set user information [item type code, VR, value]
575
- def set_user_information_array
576
- @user_information = [
577
- ["51", "UL", @max_package_size], # Max PDU Length
578
- ["52", "STR", UID], # Implementation UID
579
- ["55", "STR", NAME] # Implementation Version (Name & version)
580
- ]
581
- end
582
-
583
- end # of class
584
- end # of module