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