rtkit 0.7

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