dicom 0.7 → 0.8

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