rtkit 0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,189 @@
1
+ module RTKIT
2
+
3
+ # Contains DICOM data and methods related to a Selection of
4
+ # pixels (indices) from the binary 2D NArray of a BinImage instance.
5
+ #
6
+ # === Relations
7
+ #
8
+ # * The Selection belongs to a BinImage.
9
+ # * A Contour has many Coordinates.
10
+ #
11
+ class Selection
12
+
13
+ # The BinImage that the Selection belongs to.
14
+ attr_reader :bin_image
15
+ # An array of (general) indices.
16
+ attr_reader :indices
17
+ # An NArray of (general) indices.
18
+ #attr_reader :indices_narr
19
+
20
+ # Creates a new Selection instance from an Array (or NArray) of (general) indices.
21
+ # Returns the Selection instance.
22
+ #
23
+ # === Parameters
24
+ #
25
+ # * <tt>arr</tt> -- An Array/NArray of general indices (Integers).
26
+ # * <tt>slice</tt> -- The BinImage instance that this Selection belongs to.
27
+ #
28
+ def self.create_from_array(indices, bin_image)
29
+ raise ArgumentError, "Invalid argument 'indices'. Expected Array/NArray, got #{indices.class}." unless [NArray, Array].include?(indices.class)
30
+ raise ArgumentError, "Invalid argument 'bin_image'. Expected BinImage, got #{bin_image.class}." unless bin_image.is_a?(BinImage)
31
+ raise ArgumentError, "Invalid argument 'indices'. Expected Array to contain only integers, got #{indices.collect{|i| i.class}.uniq}." if indices.is_a?(Array) and not indices.collect{|i| i.class}.uniq == [Fixnum]
32
+ # Create the Selection:
33
+ s = self.new(bin_image)
34
+ # Set the indices:
35
+ s.add_indices(indices)
36
+ return s
37
+ end
38
+
39
+ # Creates a new Selection instance.
40
+ #
41
+ # === Parameters
42
+ #
43
+ # * <tt>bin_image</tt> -- The BinImage instance that this Selection belongs to.
44
+ #
45
+ def initialize(bin_image)
46
+ raise ArgumentError, "Invalid argument 'bin_image'. Expected BinImage, got #{bin_image.class}." unless bin_image.is_a?(BinImage)
47
+ @bin_image = bin_image
48
+ @indices = Array.new
49
+ end
50
+
51
+ # Returns true if the argument is an instance with attributes equal to self.
52
+ #
53
+ def ==(other)
54
+ if other.respond_to?(:to_selection)
55
+ other.send(:state) == state
56
+ end
57
+ end
58
+
59
+ alias_method :eql?, :==
60
+
61
+ # Adds an array of (general) indices to this Selection.
62
+ #
63
+ def add_indices(indices)
64
+ raise ArgumentError, "Invalid argument 'indices'. Expected Array/NArray, got #{indices.class}." unless [NArray, Array].include?(indices.class)
65
+ raise ArgumentError, "Invalid argument 'indices'. Expected Array to contain only integers, got #{indices.collect{|i| i.class}.uniq}." if indices.is_a?(Array) and not indices.collect{|i| i.class}.uniq == [Fixnum]
66
+ indices = indices.to_a if indices.is_a?(NArray)
67
+ @indices += indices
68
+ end
69
+
70
+ # Returns an array of column indices.
71
+ # Returns an empty array if the instance contains no indices.
72
+ #
73
+ def columns
74
+ return @indices.collect {|index| index % @bin_image.columns}
75
+ end
76
+
77
+ # Generates a Fixnum hash value for this instance.
78
+ #
79
+ def hash
80
+ state.hash
81
+ end
82
+
83
+ # Returns the length (number of indices) of this selection.
84
+ #
85
+ def length
86
+ return @indices.length
87
+ end
88
+
89
+ # Returns an array of row indices.
90
+ # Returns an empty array if the instance contains no indices.
91
+ #
92
+ def rows
93
+ return @indices.collect {|index| index / @bin_image.columns}
94
+ end
95
+
96
+ # Shifts the indices of this selection by the specified number of columns and rows.
97
+ # Positive arguments increases the column and row indices.
98
+ #
99
+ # === Restrictions
100
+ #
101
+ # NB! No out of bounds check is performed for indices
102
+ # that are shifted past the image boundary.
103
+ #
104
+ def shift(delta_col, delta_row)
105
+ raise ArgumentError, "Invalid argument 'delta_col'. Expected Integer, got #{delta_col.class}." unless delta_col.is_a?(Integer)
106
+ raise ArgumentError, "Invalid argument 'delta_row'. Expected Integer, got #{delta_row.class}." unless delta_row.is_a?(Integer)
107
+ new_columns = @indices.collect {|index| index % @bin_image.columns + delta_col}
108
+ new_rows = @indices.collect {|index| index / @bin_image.columns + delta_row}
109
+ # Set new indices:
110
+ @indices = Array.new(new_rows.length) {|i| new_columns[i] + new_rows[i] * @bin_image.columns}
111
+ end
112
+
113
+ # Shifts the indices of this selection by the specified number of columns and rows,
114
+ # virtually 'crops' the original image by 2*columns and 2*rows, and adapts the indices
115
+ # to this virtually cropped image.
116
+ #
117
+ # === Notes
118
+ #
119
+ # Negative arguments decreases the column and row indices and
120
+ # crops at the end of the columns and rows.
121
+ #
122
+ # Positive arguments increases the column and row indices and
123
+ # crops at the start of the columns and rows.
124
+ #
125
+ # === Restrictions
126
+ #
127
+ # NB! No out of bounds check is performed for indices
128
+ # that are shifted past the image boundary.
129
+ #
130
+ def shift_and_crop(delta_col, delta_row)
131
+ raise ArgumentError, "Invalid argument 'delta_col'. Expected Integer, got #{delta_col.class}." unless delta_col.is_a?(Integer)
132
+ raise ArgumentError, "Invalid argument 'delta_row'. Expected Integer, got #{delta_row.class}." unless delta_row.is_a?(Integer)
133
+ new_columns = @indices.collect {|index| index % @bin_image.columns - delta_col.abs}
134
+ new_rows = @indices.collect {|index| index / @bin_image.columns - delta_row.abs}
135
+ # Set new indices:
136
+ @indices = Array.new(new_rows.length) {|i| new_columns[i] + new_rows[i] * (@bin_image.columns - delta_col.abs * 2)}
137
+ end
138
+
139
+ # Shifts the indices of this selection by the specified number of columns.
140
+ # A positive argument increases the column indices.
141
+ #
142
+ # === Restrictions
143
+ #
144
+ # NB! No out of bounds check is performed for indices
145
+ # that are shifted past the image boundary.
146
+ #
147
+ def shift_columns(delta)
148
+ raise ArgumentError, "Invalid argument 'delta'. Expected Integer, got #{delta.class}." unless delta.is_a?(Integer)
149
+ new_columns = @indices.collect {|index| index % @bin_image.columns + delta}
150
+ new_rows = rows
151
+ # Set new indices:
152
+ @indices = Array.new(new_columns.length) {|i| new_columns[i] + new_rows[i] * @bin_image.columns}
153
+ end
154
+
155
+ # Shifts the indices of this selection by the specified number of rows.
156
+ # A positive argument increases the row indices.
157
+ #
158
+ # === Restrictions
159
+ #
160
+ # NB! No out of bounds check is performed for indices
161
+ # that are shifted past the image boundary.
162
+ #
163
+ def shift_rows(delta)
164
+ raise ArgumentError, "Invalid argument 'delta'. Expected Integer, got #{delta.class}." unless delta.is_a?(Integer)
165
+ new_columns = columns
166
+ new_rows = @indices.collect {|index| index / @bin_image.columns + delta}
167
+ # Set new indices:
168
+ @indices = Array.new(new_rows.length) {|i| new_columns[i] + new_rows[i] * @bin_image.columns}
169
+ end
170
+
171
+ # Returns self.
172
+ #
173
+ def to_selection
174
+ self
175
+ end
176
+
177
+
178
+ private
179
+
180
+
181
+ # Returns the attributes of this instance in an array (for comparison purposes).
182
+ #
183
+ def state
184
+ [@indices]
185
+ end
186
+
187
+ end
188
+
189
+ end
@@ -0,0 +1,77 @@
1
+ module RTKIT
2
+
3
+ # Contains the DICOM data and methods related to a series.
4
+ #
5
+ # === Relations
6
+ #
7
+ # * A Series belongs to a Study.
8
+ # * Some types of Series (e.g. CT, MR) have many Image instances.
9
+ #
10
+ class Series
11
+
12
+ # The SOP Class UID.
13
+ attr_reader :class_uid
14
+ # The Series Date.
15
+ attr_reader :date
16
+ # The Series Description.
17
+ attr_reader :description
18
+ # The Modality of the Series.
19
+ attr_reader :modality
20
+ # The Series's Study reference.
21
+ attr_reader :study
22
+ # The Series Instance UID.
23
+ attr_reader :series_uid
24
+ # The Series Time.
25
+ attr_reader :time
26
+
27
+ # Creates a new Series instance. The Series Instance UID string is used to uniquely identify a Series.
28
+ #
29
+ # === Parameters
30
+ #
31
+ # * <tt>series_uid</tt> -- The Series Instance UID string.
32
+ # * <tt>modality</tt> -- The Modality string of the Series, e.g. 'CT' or 'RTSTRUCT'.
33
+ # * <tt>study</tt> -- The Study instance that this Series belongs to.
34
+ # * <tt>options</tt> -- A hash of parameters.
35
+ #
36
+ # === Options
37
+ #
38
+ # * <tt>:class_uid</tt> -- String. The SOP Class UID (DICOM tag '0008,0016').
39
+ # * <tt>:date</tt> -- String. The Series Date (DICOM tag '0008,0021').
40
+ # * <tt>:time</tt> -- String. The Series Time (DICOM tag '0008,0031').
41
+ # * <tt>:description</tt> -- String. The Series Description (DICOM tag '0008,103E').
42
+ #
43
+ def initialize(series_uid, modality, study, options={})
44
+ raise ArgumentError, "Invalid argument 'uid'. Expected String, got #{series_uid.class}." unless series_uid.is_a?(String)
45
+ raise ArgumentError, "Invalid argument 'modality'. Expected String, got #{modality.class}." unless modality.is_a?(String)
46
+ raise ArgumentError, "Invalid argument 'study'. Expected Study, got #{study.class}." unless study.is_a?(Study)
47
+ # Key attributes:
48
+ @series_uid = series_uid
49
+ @modality = modality
50
+ @study = study
51
+ # Optional attributes:
52
+ @class_uid = options[:class_uid]
53
+ @date = options[:date]
54
+ @time = options[:time]
55
+ @description = options[:description]
56
+ end
57
+
58
+ # Returns true if the series is of a modality which means it contains multiple images (CT, MR, RTImage, RTDose).
59
+ # Returns false if not.
60
+ #
61
+ def image_modality?
62
+ if IMAGE_MODALITIES.include?(@modality)
63
+ return true
64
+ else
65
+ return false
66
+ end
67
+ end
68
+
69
+ # Returns the unique identifier string, which for an ImageSeries is the Series Instance UID,
70
+ # and for the other types of Series (e.g. StructureSet, Plan, etc) it is the SOP Instance UID.
71
+ #
72
+ def uid
73
+ return @sop_uid || @series_uid
74
+ end
75
+
76
+ end
77
+ end
@@ -0,0 +1,198 @@
1
+ module RTKIT
2
+
3
+ # Contains DICOM data and methods related to a patient Setup item, defined in a Plan.
4
+ #
5
+ # === Relations
6
+ #
7
+ # * A Plan has a Setup.
8
+ #
9
+ class Setup
10
+
11
+ # Patient setup number (Integer).
12
+ attr_reader :number
13
+ # Table top lateral setup displacement (Float).
14
+ attr_reader :offset_lateral
15
+ # Table top longitudinal setup displacement (Float).
16
+ attr_reader :offset_longitudinal
17
+ # Table top vertical setup displacement (Float).
18
+ attr_reader :offset_vertical
19
+ # The Plan that the Setup is defined for.
20
+ attr_reader :plan
21
+ # Patient position (orientation).
22
+ attr_reader :position
23
+ # Setup technique.
24
+ attr_reader :technique
25
+
26
+ # Creates a new Setup instance from the patient setup item of the RTPlan file.
27
+ # Returns the Setup instance.
28
+ #
29
+ # === Parameters
30
+ #
31
+ # * <tt>setup_item</tt> -- The patient setup item from the DObject of a RTPlan file.
32
+ # * <tt>plan</tt> -- The Plan instance that this Setup belongs to.
33
+ #
34
+ def self.create_from_item(setup_item, plan)
35
+ raise ArgumentError, "Invalid argument 'setup_item'. Expected DICOM::Item, got #{setup_item.class}." unless setup_item.is_a?(DICOM::Item)
36
+ raise ArgumentError, "Invalid argument 'plan'. Expected Plan, got #{plan.class}." unless plan.is_a?(Plan)
37
+ options = Hash.new
38
+ # Values from the Patient Setup Item:
39
+ position = setup_item.value(PATIENT_POSITION) || ''
40
+ number = setup_item.value(PATIENT_SETUP_NUMBER).to_i
41
+ options[:technique] = setup_item.value(SETUP_TECHNIQUE)
42
+ options[:offset_vertical] = setup_item.value(OFFSET_VERTICAL).to_f
43
+ options[:offset_longitudinal] = setup_item.value(OFFSET_LONG).to_f
44
+ options[:offset_lateral] = setup_item.value(OFFSET_LATERAL).to_f
45
+ # Create the Setup instance:
46
+ s = self.new(position, number, plan, options)
47
+ return s
48
+ end
49
+
50
+ # Creates a new Setup instance.
51
+ #
52
+ # === Parameters
53
+ #
54
+ # * <tt>position</tt> -- String. The patient position (orientation).
55
+ # * <tt>number</tt> -- Integer. The Setup number.
56
+ # * <tt>plan</tt> -- The Plan instance that this Beam belongs to.
57
+ # * <tt>options</tt> -- A hash of parameters.
58
+ #
59
+ # === Options
60
+ #
61
+ # * <tt>:technique</tt> -- String. Setup technique.
62
+ # * <tt>:offset_vertical</tt> -- Float. Table top vertical setup displacement.
63
+ # * <tt>:offset_longitudinal</tt> -- Float. Table top longitudinal setup displacement.
64
+ # * <tt>:offset_lateral</tt> -- Float. Table top lateral setup displacement.
65
+ #
66
+ def initialize(position, number, plan, options={})
67
+ raise ArgumentError, "Invalid argument 'plan'. Expected Plan, got #{plan.class}." unless plan.is_a?(Plan)
68
+ # Set values:
69
+ self.position = position
70
+ self.number = number
71
+ # Set options:
72
+ self.technique = options[:technique] if options[:technique]
73
+ self.offset_vertical = options[:offset_vertical] if options[:offset_vertical]
74
+ self.offset_longitudinal = options[:offset_longitudinal] if options[:offset_longitudinal]
75
+ self.offset_lateral = options[:offset_lateral] if options[:offset_lateral]
76
+ # Set references:
77
+ @plan = plan
78
+ # Register ourselves with the Plan:
79
+ @plan.add_setup(self)
80
+ end
81
+
82
+ # Returns true if the argument is an instance with attributes equal to self.
83
+ #
84
+ def ==(other)
85
+ if other.respond_to?(:to_setup)
86
+ other.send(:state) == state
87
+ end
88
+ end
89
+
90
+ alias_method :eql?, :==
91
+
92
+ # Generates a Fixnum hash value for this instance.
93
+ #
94
+ def hash
95
+ state.hash
96
+ end
97
+
98
+ # Sets a new patient setup number.
99
+ #
100
+ # === Parameters
101
+ #
102
+ # * <tt>value</tt> -- Float. The patient setup number.
103
+ #
104
+ def number=(value)
105
+ raise ArgumentError, "Invalid argument 'value'. Expected Integer, got #{value.class}." unless value.is_a?(Integer)
106
+ @number = value
107
+ end
108
+
109
+ # Sets a new table top lateral setup displacement.
110
+ #
111
+ # === Parameters
112
+ #
113
+ # * <tt>value</tt> -- Float. The table top lateral setup displacement.
114
+ #
115
+ def offset_lateral=(value)
116
+ raise ArgumentError, "Invalid argument 'value'. Expected Float, got #{value.class}." unless value.is_a?(Float)
117
+ @offset_lateral = value
118
+ end
119
+
120
+ # Sets a new table top longitudinal setup displacement.
121
+ #
122
+ # === Parameters
123
+ #
124
+ # * <tt>value</tt> -- Float. The table top longitudinal setup displacement.
125
+ #
126
+ def offset_longitudinal=(value)
127
+ raise ArgumentError, "Invalid argument 'value'. Expected Float, got #{value.class}." unless value.is_a?(Float)
128
+ @offset_longitudinal = value
129
+ end
130
+
131
+ # Sets a new table top vertical setup displacement.
132
+ #
133
+ # === Parameters
134
+ #
135
+ # * <tt>value</tt> -- Float. The table top vertical setup displacement.
136
+ #
137
+ def offset_vertical=(value)
138
+ raise ArgumentError, "Invalid argument 'value'. Expected Float, got #{value.class}." unless value.is_a?(Float)
139
+ @offset_vertical = value
140
+ end
141
+
142
+ # Sets a new patient position.
143
+ #
144
+ # === Parameters
145
+ #
146
+ # * <tt>value</tt> -- String. The patient position.
147
+ #
148
+ def position=(value)
149
+ raise ArgumentError, "Invalid argument 'value'. Expected String, got #{value.class}." unless value.is_a?(String)
150
+ @position = value
151
+ end
152
+
153
+ # Sets a new setup technique.
154
+ #
155
+ # === Parameters
156
+ #
157
+ # * <tt>value</tt> -- String. The setup technique.
158
+ #
159
+ def technique=(value)
160
+ raise ArgumentError, "Invalid argument 'value'. Expected String, got #{value.class}." unless value.is_a?(String)
161
+ @technique = value
162
+ end
163
+
164
+ # Creates and returns a Patient Setup Sequence Item from the attributes of the Setup.
165
+ #
166
+ def to_item
167
+ item = DICOM::Item.new
168
+ item.add(DICOM::Element.new(ROI_COLOR, @color))
169
+ s = DICOM::Sequence.new(CONTOUR_SQ)
170
+ item.add(s)
171
+ item.add(DICOM::Element.new(REF_ROI_NUMBER, @number.to_s))
172
+ # Add Contour items to the Contour Sequence (one or several items per Slice):
173
+ @slices.each do |slice|
174
+ slice.contours.each do |contour|
175
+ s.add_item(contour.to_item)
176
+ end
177
+ end
178
+ return item
179
+ end
180
+
181
+ # Returns self.
182
+ #
183
+ def to_setup
184
+ self
185
+ end
186
+
187
+
188
+ private
189
+
190
+
191
+ # Returns the attributes of this instance in an array (for comparison purposes).
192
+ #
193
+ def state
194
+ [@number, @offset_lateral, @offset_longitudinal, @offset_vertical, @position, @technique]
195
+ end
196
+
197
+ end
198
+ end