rtkit 0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +10 -0
- data/COPYING +674 -0
- data/README.rdoc +107 -0
- data/lib/rtkit.rb +68 -0
- data/lib/rtkit/beam.rb +346 -0
- data/lib/rtkit/bin_image.rb +578 -0
- data/lib/rtkit/bin_matcher.rb +241 -0
- data/lib/rtkit/bin_volume.rb +263 -0
- data/lib/rtkit/collimator.rb +157 -0
- data/lib/rtkit/collimator_setup.rb +143 -0
- data/lib/rtkit/constants.rb +215 -0
- data/lib/rtkit/contour.rb +213 -0
- data/lib/rtkit/control_point.rb +371 -0
- data/lib/rtkit/coordinate.rb +83 -0
- data/lib/rtkit/data_set.rb +264 -0
- data/lib/rtkit/dose.rb +70 -0
- data/lib/rtkit/dose_distribution.rb +206 -0
- data/lib/rtkit/dose_volume.rb +280 -0
- data/lib/rtkit/frame.rb +164 -0
- data/lib/rtkit/image.rb +372 -0
- data/lib/rtkit/image_series.rb +290 -0
- data/lib/rtkit/logging.rb +158 -0
- data/lib/rtkit/methods.rb +105 -0
- data/lib/rtkit/mixins/image_parent.rb +40 -0
- data/lib/rtkit/patient.rb +229 -0
- data/lib/rtkit/pixel_data.rb +237 -0
- data/lib/rtkit/plan.rb +259 -0
- data/lib/rtkit/plane.rb +165 -0
- data/lib/rtkit/roi.rb +388 -0
- data/lib/rtkit/rt_dose.rb +237 -0
- data/lib/rtkit/rt_image.rb +179 -0
- data/lib/rtkit/ruby_extensions.rb +165 -0
- data/lib/rtkit/selection.rb +189 -0
- data/lib/rtkit/series.rb +77 -0
- data/lib/rtkit/setup.rb +198 -0
- data/lib/rtkit/slice.rb +184 -0
- data/lib/rtkit/staple.rb +305 -0
- data/lib/rtkit/structure_set.rb +442 -0
- data/lib/rtkit/study.rb +214 -0
- data/lib/rtkit/variables.rb +23 -0
- data/lib/rtkit/version.rb +6 -0
- metadata +159 -0
@@ -0,0 +1,241 @@
|
|
1
|
+
module RTKIT
|
2
|
+
|
3
|
+
# Contains the DICOM data and methods related to the comparison of binary volumes (i.e. segmented volumes).
|
4
|
+
#
|
5
|
+
class BinMatcher
|
6
|
+
|
7
|
+
# The reference (master) BinVolume.
|
8
|
+
attr_reader :master
|
9
|
+
# An array of BinVolumes.
|
10
|
+
attr_reader :volumes
|
11
|
+
|
12
|
+
# Creates a new BinMatcher instance.
|
13
|
+
#
|
14
|
+
# === Parameters
|
15
|
+
#
|
16
|
+
# * <tt>volumes</tt> -- An array of BinVolume instances to be matched.
|
17
|
+
# * <tt>master</tt> -- A master BinVolume which the other volumes will be compared against.
|
18
|
+
#
|
19
|
+
def initialize(volumes=nil, master=nil)
|
20
|
+
raise ArgumentError, "Invalid argument 'volumes'. Expected Array, got #{volumes.class}." if volumes && volumes.class != Array
|
21
|
+
raise ArgumentError, "Invalid argument 'master'. Expected BinVolume, got #{master.class}." if master && master.class != BinVolume
|
22
|
+
raise ArgumentError, "Invalid argument 'volumes'. Expected array to contain only BinVolume instances, got #{volumes.collect{|i| i.class}.uniq}." if volumes && volumes.collect{|i| i.class}.uniq != [BinVolume]
|
23
|
+
@volumes = volumes || Array.new
|
24
|
+
@master = master
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns true if the argument is an instance with attributes equal to self.
|
28
|
+
#
|
29
|
+
def ==(other)
|
30
|
+
if other.respond_to?(:to_bin_matcher)
|
31
|
+
other.send(:state) == state
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
alias_method :eql?, :==
|
36
|
+
|
37
|
+
# Adds a BinVolume instance to the matcher.
|
38
|
+
#
|
39
|
+
def add(volume)
|
40
|
+
raise ArgumentError, "Invalid argument 'volume'. Expected BinVolume, got #{volume.class}." unless volume.is_a?(BinVolume)
|
41
|
+
@volumes << volume
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns an array of volumes, (reversly) sorted by their sensitivity score.
|
45
|
+
# The volumes are sorted in such a way that the best scoring volume (highest sensitivity) appears first.
|
46
|
+
# If no volumes are defined, an empty array is returned.
|
47
|
+
#
|
48
|
+
def by_sensitivity
|
49
|
+
return (@volumes.sort_by {|v| v.sensitivity}).reverse
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns an array of volumes, (reversly) sorted by their specificity score.
|
53
|
+
# The volumes are sorted in such a way that the best scoring volume (highest specificity) appears first.
|
54
|
+
# If no volumes are defined, an empty array is returned.
|
55
|
+
#
|
56
|
+
def by_specificity
|
57
|
+
return (@volumes.sort_by {|v| v.specificity}).reverse
|
58
|
+
end
|
59
|
+
|
60
|
+
# Ensures that a valid comparison can be done by making sure that every volume
|
61
|
+
# has a BinImage for any image that is referenced among the BinVolumes.
|
62
|
+
# If one or more BinVolumes are missing one or more BinImages,
|
63
|
+
# empty BinImages will be created for these BinVolumes.
|
64
|
+
#
|
65
|
+
# === Notes
|
66
|
+
#
|
67
|
+
# * The master volume (if present) is also processed in this method.
|
68
|
+
#
|
69
|
+
def fill_blanks
|
70
|
+
if @volumes.length > 0
|
71
|
+
# Register all unique images referenced by the various volumes:
|
72
|
+
images = Set.new
|
73
|
+
# Include the master volume (if it is present):
|
74
|
+
[@volumes, @master].flatten.compact.each do |volume|
|
75
|
+
volume.bin_images.each do |bin_image|
|
76
|
+
images << bin_image.image unless images.include?(bin_image.image)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
# Check if any of the volumes have images missing, and if so, create empty images:
|
80
|
+
images.each do |image|
|
81
|
+
[@volumes, @master].flatten.compact.each do |volume|
|
82
|
+
match = false
|
83
|
+
volume.bin_images.each do |bin_image|
|
84
|
+
match = true if bin_image.image == image
|
85
|
+
end
|
86
|
+
unless match
|
87
|
+
# Create BinImage:
|
88
|
+
bin_img = BinImage.new(NArray.byte(image.columns, image.rows), image)
|
89
|
+
volume.add(bin_img)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Generates a Fixnum hash value for this instance.
|
97
|
+
#
|
98
|
+
def hash
|
99
|
+
state.hash
|
100
|
+
end
|
101
|
+
|
102
|
+
# Sets the master (reference) BinVolume.
|
103
|
+
#
|
104
|
+
def master=(volume)
|
105
|
+
raise ArgumentError, "Invalid argument 'volume'. Expected BinVolume, got #{volume.class}." unless volume.is_a?(BinVolume)
|
106
|
+
@master = volume
|
107
|
+
end
|
108
|
+
|
109
|
+
# Returns an array of NArrays from the BinVolumes of this instance.
|
110
|
+
# Returns an empty array if no volumes are defined.
|
111
|
+
#
|
112
|
+
# === Notes
|
113
|
+
#
|
114
|
+
# * Only the volumes are returned. The master volume (if present) is not returned with this method.
|
115
|
+
#
|
116
|
+
def narrays(sort_slices=true)
|
117
|
+
n = Array.new
|
118
|
+
@volumes.each do |volume|
|
119
|
+
n << volume.narray(sort_slices)
|
120
|
+
end
|
121
|
+
return n
|
122
|
+
end
|
123
|
+
|
124
|
+
=begin
|
125
|
+
# Along each dimension of the input binary volume(s), removes any index (i.e. slice, column or row)
|
126
|
+
# which is empty in all volumes (including the master volume, if specified). This method results
|
127
|
+
# in reduced volumes, corresponding to a clipbox which tightly encloses the defined (positive) volume
|
128
|
+
# (indices with pixel value 1 in at least one of the volumes). Such a reduced volume may yield e.g.
|
129
|
+
# specificity scores with better contrast.
|
130
|
+
#
|
131
|
+
def remove_empty_indices
|
132
|
+
# It only makes sense to run this volume reduction if the number of dimensions are 2 or more:
|
133
|
+
volumes = [*@volumes, master].compact
|
134
|
+
# For each volume the indices typically correspond to the following
|
135
|
+
# descriptions: slice, column & row
|
136
|
+
volumes.first.narray.shape.each_with_index do |size, dim_index|
|
137
|
+
extract = Array.new(volumes.first.narray.dim, true)
|
138
|
+
positive_indices = Array.new
|
139
|
+
size.times do |i|
|
140
|
+
extract[dim_index] = i
|
141
|
+
positive = false
|
142
|
+
volumes.each do |volume|
|
143
|
+
positive = true if volume.narray[*extract].max > 0
|
144
|
+
end
|
145
|
+
positive_indices << i if positive
|
146
|
+
end
|
147
|
+
extract[dim_index] = positive_indices
|
148
|
+
if positive_indices.length < size
|
149
|
+
volumes.each do |volume|
|
150
|
+
volume.narr = volume.narr[*extract]
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
volumes.each {|vol| puts vol.narr.inspect}
|
155
|
+
volumes.each {|vol| puts vol.narr.shape}
|
156
|
+
@volumes.each {|vol| puts vol.narr.shape}
|
157
|
+
end
|
158
|
+
=end
|
159
|
+
|
160
|
+
# Scores the volumes of the BinMatcher instance against the reference (master) volume,
|
161
|
+
# by using Dice's coeffecient.
|
162
|
+
#
|
163
|
+
# For more information, see:
|
164
|
+
# http://en.wikipedia.org/wiki/Dice%27s_coefficient
|
165
|
+
#
|
166
|
+
def score_dice
|
167
|
+
if @master
|
168
|
+
# Find the voxel-indices for the master where the decisions are 1 and 0:
|
169
|
+
pos_indices_master = (@master.narray.eq 1).where
|
170
|
+
@volumes.each do |bin_vol|
|
171
|
+
pos_indices_vol = (bin_vol.narray.eq 1).where
|
172
|
+
num_common = (@master.narray[pos_indices_vol].eq 1).where.length
|
173
|
+
bin_vol.dice = 2 * num_common / (pos_indices_master.length + pos_indices_vol.length).to_f
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Scores the volumes of the BinMatcher instance against the reference (master) volume,
|
179
|
+
# by using Sensitivity and Specificity.
|
180
|
+
#
|
181
|
+
# For more information, see:
|
182
|
+
# http://en.wikipedia.org/wiki/Sensitivity_and_specificity
|
183
|
+
#
|
184
|
+
def score_ss
|
185
|
+
if @master
|
186
|
+
# Find the voxel-indices for the master where the decisions are 1 and 0:
|
187
|
+
pos_indices, neg_indices = (@master.narray.eq 1).where2
|
188
|
+
@volumes.each do |bin_vol|
|
189
|
+
narr = bin_vol.narray
|
190
|
+
bin_vol.sensitivity = (narr[pos_indices].eq 1).where.length / pos_indices.length.to_f
|
191
|
+
bin_vol.specificity = (narr[neg_indices].eq 0).where.length / neg_indices.length.to_f
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Rearranges the BinImages belonging to the BinVolumes of this instance,
|
197
|
+
# by matching the BinImages by their Image instance references,
|
198
|
+
# to ensure that the NArrays extracted from these volumes are truly comparable.
|
199
|
+
#
|
200
|
+
# === Notes
|
201
|
+
#
|
202
|
+
# * The master volume (if present) is not processed in this method.
|
203
|
+
# * Raises an exception if any irregularities in number of BinImages or Image references occurs.
|
204
|
+
#
|
205
|
+
def sort_volumes
|
206
|
+
# It only makes sense to sort if we have at least two volumes:
|
207
|
+
if @volumes.length > 1
|
208
|
+
raise "All volumes of the BinMatcher isntance must have the same number of BinImages (got lengths #{@volumes.collect {|v| v.bin_images.length}})." if @volumes.collect {|v| v.bin_images.length}.uniq.length > 1
|
209
|
+
# Collect the Image references of the BinImage's of the first volume:
|
210
|
+
desired_image_order = Array.new
|
211
|
+
@volumes.first.bin_images.collect {|bin_image| desired_image_order << bin_image.image}
|
212
|
+
raise "One (or more) Image references were nil in the first BinVolume instance of the BinMatcher. Unable to sort BinImages when they lack Image reference." if desired_image_order.compact.length != desired_image_order.length
|
213
|
+
# Sort the BinImages of the remaining volumes so that their order of images are equal to that of the first volume:
|
214
|
+
(1...@volumes.length).each do |i|
|
215
|
+
current_image_order = Array.new
|
216
|
+
@volumes[i].bin_images.collect {|bin_image| current_image_order << bin_image.image}
|
217
|
+
sort_order = current_image_order.compare_with(desired_image_order)
|
218
|
+
@volumes[i].bin_images.sort_by_order!(sort_order)
|
219
|
+
#@volumes[i].reorder_images(sort_order)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# Returns self.
|
225
|
+
#
|
226
|
+
def to_bin_matcher
|
227
|
+
self
|
228
|
+
end
|
229
|
+
|
230
|
+
|
231
|
+
private
|
232
|
+
|
233
|
+
|
234
|
+
# Returns the attributes of this instance in an array (for comparison purposes).
|
235
|
+
#
|
236
|
+
def state
|
237
|
+
[@volumes, @master]
|
238
|
+
end
|
239
|
+
|
240
|
+
end
|
241
|
+
end
|
@@ -0,0 +1,263 @@
|
|
1
|
+
module RTKIT
|
2
|
+
|
3
|
+
# Contains the DICOM data and methods related to a binary volume.
|
4
|
+
#
|
5
|
+
# === Inheritance
|
6
|
+
#
|
7
|
+
# * As the BinVolume class inherits from the PixelData class, all PixelData methods are available to instances of BinVolume.
|
8
|
+
#
|
9
|
+
class BinVolume < PixelData
|
10
|
+
|
11
|
+
# An array of BinImage references.
|
12
|
+
attr_reader :bin_images
|
13
|
+
# Dice's Coeffecient.
|
14
|
+
attr_accessor :dice
|
15
|
+
# A NArray associated with this BinVolume (NB! This is a cached copy. If you want to ensure to
|
16
|
+
# extract an narray which corresponds to the BinVolume's Images, use the narray method instead.
|
17
|
+
attr_accessor :narr
|
18
|
+
# A score (fraction) of the segmented volume compared to a master volume, ranging from 0 (worst) to 1 (all true voxels segmented in this volume).
|
19
|
+
attr_accessor :sensitivity
|
20
|
+
# The BinVolume's series reference (e.g. ImageSeries or DoseVolume).
|
21
|
+
attr_reader :series
|
22
|
+
# A reference to the source of this BinaryVolume (ROI or Dose instance).
|
23
|
+
attr_reader :source
|
24
|
+
# A score (fraction) of the segmented volume compared to a master volume, ranging from 0 (worst) to 1 (none of the true remaining voxels are segmented in this volume).
|
25
|
+
attr_accessor :specificity
|
26
|
+
|
27
|
+
# Creates a new BinVolume instance from a DoseVolume.
|
28
|
+
# The BinVolume is typically defined from a ROI delineation against an image series,
|
29
|
+
# but it may also be applied to an rtdose 'image' series.
|
30
|
+
# Returns the BinVolume instance.
|
31
|
+
#
|
32
|
+
# === Notes
|
33
|
+
#
|
34
|
+
# * Even though listed as optional parameters, at least one of the min and max
|
35
|
+
# options must be specified in order to construct a valid binary volume.
|
36
|
+
#
|
37
|
+
# === Parameters
|
38
|
+
#
|
39
|
+
# * <tt>image_volume</tt> -- The image volume which the binary volume will be based on (a DoseVolume or an ImageSeries).
|
40
|
+
# * <tt>min</tt> -- Float. An optional lower dose limit from which to define the the binary volume.
|
41
|
+
# * <tt>max</tt> -- Float. An optional upper dose limit from which to define the the binary volume.
|
42
|
+
#
|
43
|
+
def self.from_dose(dose_volume, min=nil, max=nil, image_volume)
|
44
|
+
raise ArgumentError, "Invalid argument 'dose_volume'. Expected DoseVolume, got #{dose_volume.class}." unless dose_volume.class == DoseVolume
|
45
|
+
raise ArgumentError, "Invalid argument 'image_volume'. Expected ImageSeries or DoseVolume, got #{image_volume.class}." unless [ImageSeries, DoseVolume].include?(image_volume.class)
|
46
|
+
raise ArgumentError "Need at least one dose limit parameter. Neither min nor max was specified." unless min or max
|
47
|
+
# Create the BinVolume instance:
|
48
|
+
bv = self.new(dose_volume) # FIXME: Specify dose limit somehow here?!
|
49
|
+
# Add BinImages for each of the DoseVolume's images:
|
50
|
+
dose_narr = dose_volume.dose_arr
|
51
|
+
dose_volume.images.each_index do |i|
|
52
|
+
#ref_image = image_volume.image(dose_img.pos_slice)
|
53
|
+
ref_image = image_volume.images[i]
|
54
|
+
dose_image = dose_narr[i, true, true]
|
55
|
+
# Create the bin narray:
|
56
|
+
narr = NArray.byte(ref_image.columns, ref_image.rows)
|
57
|
+
if !min
|
58
|
+
# Only the max limit is specified:
|
59
|
+
marked_indices = (dose_image.le max.to_f)
|
60
|
+
elsif !max
|
61
|
+
# Only the min limit is specified:
|
62
|
+
marked_indices = (dose_image.ge min.to_f)
|
63
|
+
else
|
64
|
+
# Both min and max limits are specified:
|
65
|
+
smaller = (dose_image.le max.to_f)
|
66
|
+
bigger = (dose_image.ge min.to_f)
|
67
|
+
marked_indices = smaller.and bigger
|
68
|
+
end
|
69
|
+
narr[marked_indices] = 1
|
70
|
+
bin_img = BinImage.new(narr, ref_image)
|
71
|
+
bv.add(bin_img)
|
72
|
+
end
|
73
|
+
return bv
|
74
|
+
end
|
75
|
+
|
76
|
+
# Creates a new BinVolume instance from a ROI.
|
77
|
+
# The BinVolume is typically defined from a ROI delineation against an image series,
|
78
|
+
# but it may also be applied to an rtdose 'image' series.
|
79
|
+
# Returns the BinVolume instance.
|
80
|
+
#
|
81
|
+
# === Parameters
|
82
|
+
#
|
83
|
+
# * <tt>roi</tt> -- A ROI from which to define the binary volume.
|
84
|
+
# * <tt>image_volume</tt> -- The image volume which the binary volume will be based on (an ImageSeries or a DoseVolume).
|
85
|
+
#
|
86
|
+
def self.from_roi(roi, image_volume)
|
87
|
+
raise ArgumentError, "Invalid argument 'roi'. Expected ROI, got #{roi.class}." unless roi.is_a?(ROI)
|
88
|
+
raise ArgumentError, "Invalid argument 'image_volume'. Expected ImageSeries or DoseVolume, got #{image_volume.class}." unless [ImageSeries, DoseVolume].include?(image_volume.class)
|
89
|
+
# Create the BinVolume instance:
|
90
|
+
bv = self.new(roi.image_series, :source => roi)
|
91
|
+
# Add BinImages for each of the ROIs slices:
|
92
|
+
roi.slices.each do |slice|
|
93
|
+
image = image_volume.image(slice.pos)
|
94
|
+
BinImage.from_contours(slice.contours, image, bv)
|
95
|
+
#bv.add(slice.bin_image)
|
96
|
+
end
|
97
|
+
return bv
|
98
|
+
end
|
99
|
+
|
100
|
+
# Creates a new BinVolume instance from an image series (i.e. ImageSeries or DoseVolume).
|
101
|
+
# A BinVolume created this way specified the entire volume: i.e. the Binvolume
|
102
|
+
# has the same dimensions as the image series, and all pixels are 1.
|
103
|
+
# Returns the BinVolume instance.
|
104
|
+
#
|
105
|
+
# === Parameters
|
106
|
+
#
|
107
|
+
# * <tt>image_volume</tt> -- The image volume which the binary volume will be based on (an ImageSeries or a DoseVolume).
|
108
|
+
#
|
109
|
+
def self.from_volume(image_volume)
|
110
|
+
raise ArgumentError, "Invalid argument 'image_volume'. Expected ImageSeries or DoseVolume, got #{image_volume.class}." unless [ImageSeries, DoseVolume].include?(image_volume.class)
|
111
|
+
# Create the BinVolume instance:
|
112
|
+
bv = self.new(image_volume)
|
113
|
+
# Add BinImages for each of the ROIs slices:
|
114
|
+
image_volume.images.each do |image|
|
115
|
+
# Make an NArray of proper size filled with ones:
|
116
|
+
narr = NArray.byte(image.columns, image.rows).fill(1)
|
117
|
+
# Create the BinImage instance and add it to the BinVolume:
|
118
|
+
bin_img = BinImage.new(narr, image)
|
119
|
+
bv.add(bin_img)
|
120
|
+
end
|
121
|
+
return bv
|
122
|
+
end
|
123
|
+
|
124
|
+
# Creates a new BinVolume instance.
|
125
|
+
#
|
126
|
+
# === Parameters
|
127
|
+
#
|
128
|
+
# * <tt>series</tt> -- The image series (e.g. ImageSeries or DoseVolume) which forms the reference data of the BinVolume.
|
129
|
+
# * <tt>options</tt> -- A hash of parameters.
|
130
|
+
#
|
131
|
+
# === Options
|
132
|
+
#
|
133
|
+
# * <tt>:images</tt> -- An array of BinImage instances that is assigned to this BinVolume.
|
134
|
+
# * <tt>:source</tt> -- The object which is the source of the binary (segmented) data (i.e. ROI or Dose/Hounsfield threshold).
|
135
|
+
#
|
136
|
+
def initialize(series, options={})
|
137
|
+
raise ArgumentError, "Invalid argument 'series'. Expected ImageSeries or DoseVolume, got #{series.class}." unless [ImageSeries, DoseVolume].include?(series.class)
|
138
|
+
raise ArgumentError, "Invalid option 'images'. Expected Array, got #{options[:images].class}." if options[:images] && options[:images].class != Array
|
139
|
+
raise ArgumentError, "Invalid option 'source'. Expected ROI or RTDose, got #{options[:source].class}." if options[:source] && ![ROI, RTDose].include?(options[:source].class)
|
140
|
+
raise ArgumentError, "Invalid option 'images'. Expected only BinImage instances in the array, got #{options[:images].collect{|i| i.class}.uniq}." if options[:images] && options[:images].collect{|i| i.class}.uniq.length > 1
|
141
|
+
@series = series
|
142
|
+
@bin_images = options[:images] || Array.new
|
143
|
+
@source = options[:source]
|
144
|
+
end
|
145
|
+
|
146
|
+
# Returns true if the argument is an instance with attributes equal to self.
|
147
|
+
#
|
148
|
+
def ==(other)
|
149
|
+
if other.respond_to?(:to_bin_volume)
|
150
|
+
other.send(:state) == state
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
alias_method :eql?, :==
|
155
|
+
|
156
|
+
# Adds a BinImage instance to the volume.
|
157
|
+
#
|
158
|
+
def add(bin_image)
|
159
|
+
raise ArgumentError, "Invalid argument 'bin_image'. Expected BinImage, got #{bin_image.class}." unless bin_image.is_a?(BinImage)
|
160
|
+
@bin_images << bin_image
|
161
|
+
end
|
162
|
+
|
163
|
+
# Returns the number of columns in the images of the volume.
|
164
|
+
#
|
165
|
+
def columns
|
166
|
+
return @bin_images.first.columns if @bin_images.first
|
167
|
+
end
|
168
|
+
|
169
|
+
# Returns the number of frames (slices) in the set of images that makes up this volume.
|
170
|
+
#
|
171
|
+
def frames
|
172
|
+
return @bin_images.length
|
173
|
+
end
|
174
|
+
|
175
|
+
# Generates a Fixnum hash value for this instance.
|
176
|
+
#
|
177
|
+
def hash
|
178
|
+
state.hash
|
179
|
+
end
|
180
|
+
|
181
|
+
# Returns 3d volume array consisting of the 2d Narray images from the BinImage instances that makes up this volume.
|
182
|
+
# Returns nil if no BinImage instances are connected to this BinVolume.
|
183
|
+
#
|
184
|
+
def narray(sort_slices=true)
|
185
|
+
if @bin_images.length > 0
|
186
|
+
# Determine the slice position of each BinImage:
|
187
|
+
locations = Array.new
|
188
|
+
if sort_slices
|
189
|
+
@bin_images.collect {|image| locations << image.pos_slice}
|
190
|
+
images = @bin_images.sort_by_order(locations.sort_order)
|
191
|
+
else
|
192
|
+
images = @bin_images
|
193
|
+
end
|
194
|
+
# Create volume array and fill in the images:
|
195
|
+
volume = NArray.byte(frames, columns, rows)
|
196
|
+
images.each_with_index do |sorted_image, i|
|
197
|
+
volume[i, true, true] = sorted_image.narray
|
198
|
+
end
|
199
|
+
@narr = volume
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def reorder_images(order)
|
204
|
+
@bin_images = @bin_images.sort_by_order(order)
|
205
|
+
end
|
206
|
+
|
207
|
+
# Returns the number of rows in the images of the volume.
|
208
|
+
#
|
209
|
+
def rows
|
210
|
+
return @bin_images.first.rows if @bin_images.first
|
211
|
+
end
|
212
|
+
|
213
|
+
# Returns self.
|
214
|
+
#
|
215
|
+
def to_bin_volume
|
216
|
+
self
|
217
|
+
end
|
218
|
+
|
219
|
+
# Creates a ROI instance from the segmentation of this BinVolume.
|
220
|
+
# Returns the ROI instance.
|
221
|
+
#
|
222
|
+
# === Parameters
|
223
|
+
#
|
224
|
+
# * <tt>struct</tt> -- A StructureSet instance which the ROI will be connected to.
|
225
|
+
# * <tt>options</tt> -- A hash of parameters.
|
226
|
+
#
|
227
|
+
# === Options
|
228
|
+
#
|
229
|
+
# * <tt>:algorithm</tt> -- String. The ROI Generation Algorithm. Defaults to 'Automatic'.
|
230
|
+
# * <tt>:name</tt> -- String. The ROI Name. Defaults to 'BinVolume'.
|
231
|
+
# * <tt>:number</tt> -- Integer. The ROI Number. Defaults to the first available ROI Number in the StructureSet.
|
232
|
+
# * <tt>:interpreter</tt> -- String. The ROI Interpreter. Defaults to 'RTKIT'.
|
233
|
+
# * <tt>:type</tt> -- String. The ROI Interpreted Type. Defaults to 'CONTROL'.
|
234
|
+
#
|
235
|
+
def to_roi(struct, options={})
|
236
|
+
raise ArgumentError, "Invalid argument 'struct'. Expected StructureSet, got #{struct.class}." unless struct.is_a?(StructureSet)
|
237
|
+
# Set values:
|
238
|
+
algorithm = options[:algorithm]
|
239
|
+
name = options[:name] || 'BinVolume'
|
240
|
+
number = options[:number]
|
241
|
+
interpreter = options[:interpreter]
|
242
|
+
type = options[:type]
|
243
|
+
# Create the ROI:
|
244
|
+
roi = struct.create_roi(@series.frame, :algorithm => algorithm, :name => name, :number => number, :interpreter => interpreter, :type => type)
|
245
|
+
# Create Slices:
|
246
|
+
@bin_images.each do |bin_image|
|
247
|
+
bin_image.to_slice(roi)
|
248
|
+
end
|
249
|
+
return roi
|
250
|
+
end
|
251
|
+
|
252
|
+
|
253
|
+
private
|
254
|
+
|
255
|
+
|
256
|
+
# Returns the attributes of this instance in an array (for comparison purposes).
|
257
|
+
#
|
258
|
+
def state
|
259
|
+
[@bin_images, @dice, @narr, @sensitivity, @specificity]
|
260
|
+
end
|
261
|
+
|
262
|
+
end
|
263
|
+
end
|