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,40 @@
|
|
1
|
+
module RTKIT
|
2
|
+
|
3
|
+
# This is a mixin-module for the classes that are image 'parents',
|
4
|
+
# i.e. they contain a reference to an array of image instances.
|
5
|
+
#
|
6
|
+
# This module is mixed in by the ImageSeries and Volume classes.
|
7
|
+
#
|
8
|
+
module ImageParent
|
9
|
+
|
10
|
+
# Returns the slice spacing (a float value in units of mm), which describes
|
11
|
+
# the distance between two neighbouring images in this image series.
|
12
|
+
# NB! If the image series contains 0 or 1 image, a slice spacing can not be
|
13
|
+
# determined and nil is returned.
|
14
|
+
#
|
15
|
+
def slice_spacing
|
16
|
+
if @slice_spacing
|
17
|
+
# If the slice spacing has already been computed, return it instead of recomputing:
|
18
|
+
return @slice_spacing
|
19
|
+
else
|
20
|
+
if @images.length > 1
|
21
|
+
# Collect slice positions:
|
22
|
+
slice_positions = NArray.to_na(@images.collect{|image| image.pos_slice})
|
23
|
+
spacings = (slice_positions[1..-1] - slice_positions[0..-2]).abs
|
24
|
+
@slice_spacing = spacings.to_a.most_common_value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Updates the position that is registered for the image for this series.
|
30
|
+
#
|
31
|
+
def update_image_position(image, new_pos)
|
32
|
+
raise ArgumentError, "Invalid argument 'image'. Expected Image, got #{image.class}." unless image.is_a?(Image)
|
33
|
+
# Remove old position key:
|
34
|
+
@image_positions.delete(image.pos_slice)
|
35
|
+
# Add the new position:
|
36
|
+
@image_positions[new_pos] = image
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,229 @@
|
|
1
|
+
module RTKIT
|
2
|
+
|
3
|
+
# Contains the DICOM data and methods related to a patient.
|
4
|
+
#
|
5
|
+
# === Relations
|
6
|
+
#
|
7
|
+
# * A Patient has many Study instances.
|
8
|
+
#
|
9
|
+
class Patient
|
10
|
+
|
11
|
+
# The Patient's birth date.
|
12
|
+
attr_reader :birth_date
|
13
|
+
# The DataSet which this Patient belongs to.
|
14
|
+
attr_reader :dataset
|
15
|
+
# An array of Frame (of Reference) instances belonging to this Patient.
|
16
|
+
attr_reader :frames
|
17
|
+
# The Patient's ID (string).
|
18
|
+
attr_reader :id
|
19
|
+
# The Patient's name.
|
20
|
+
attr_reader :name
|
21
|
+
# The Patient's sex.
|
22
|
+
attr_reader :sex
|
23
|
+
# An array of Study references.
|
24
|
+
attr_reader :studies
|
25
|
+
|
26
|
+
# Creates a new Patient instance by loading patient information from the specified DICOM object.
|
27
|
+
# The Patient's ID string value is used to uniquely identify a patient.
|
28
|
+
#
|
29
|
+
# === Parameters
|
30
|
+
#
|
31
|
+
# * <tt>dcm</tt> -- An instance of a DICOM object (DICOM::DObject).
|
32
|
+
# * <tt>dataset</tt> -- The DataSet instance that this Patient belongs to.
|
33
|
+
#
|
34
|
+
def self.load(dcm, dataset)
|
35
|
+
raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
|
36
|
+
raise ArgumentError, "Invalid argument 'dataset'. Expected DataSet, got #{dataset.class}." unless dataset.is_a?(DataSet)
|
37
|
+
name = dcm.value(PATIENTS_NAME)
|
38
|
+
id = dcm.value(PATIENTS_ID)
|
39
|
+
birth_date = dcm.value(BIRTH_DATE)
|
40
|
+
sex = dcm.value(SEX)
|
41
|
+
pat = self.new(name, id, dataset, :birth_date => birth_date, :sex => sex)
|
42
|
+
pat.add(dcm)
|
43
|
+
return pat
|
44
|
+
end
|
45
|
+
|
46
|
+
# Creates a new Patient instance. The Patient's ID string is used to uniquely identify a patient.
|
47
|
+
#
|
48
|
+
# === Parameters
|
49
|
+
#
|
50
|
+
# * <tt>name</tt> -- String. The Name of the Patient.
|
51
|
+
# * <tt>id</tt> -- String. The ID of the Patient.
|
52
|
+
# * <tt>dataset</tt> -- The DataSet instance which the Patient is associated with.
|
53
|
+
# * <tt>options</tt> -- A hash of parameters.
|
54
|
+
#
|
55
|
+
# === Options
|
56
|
+
#
|
57
|
+
# * <tt>:birth_date</tt> -- A Date String of the Patient's birth date.
|
58
|
+
# * <tt>:sex</tt> -- A code string indicating the Patient's sex.
|
59
|
+
#
|
60
|
+
def initialize(name, id, dataset, options={})
|
61
|
+
raise ArgumentError, "Invalid argument 'name'. Expected String, got #{name.class}." unless name.is_a?(String)
|
62
|
+
raise ArgumentError, "Invalid argument 'id'. Expected String, got #{id.class}." unless id.is_a?(String)
|
63
|
+
raise ArgumentError, "Invalid argument 'dataset'. Expected DataSet, got #{dataset.class}." unless dataset.is_a?(DataSet)
|
64
|
+
raise ArgumentError, "Invalid option ':birth_date'. Expected String, got #{options[:birth_date].class}." if options[:birth_date] && !options[:birth_date].is_a?(String)
|
65
|
+
raise ArgumentError, "Invalid option ':sex'. Expected String, got #{options[:sex].class}." if options[:sex] && !options[:sex].is_a?(String)
|
66
|
+
# Key attributes:
|
67
|
+
@name = name
|
68
|
+
@id = id
|
69
|
+
@dataset = dataset
|
70
|
+
# Default attributes:
|
71
|
+
@frames = Array.new
|
72
|
+
@studies = Array.new
|
73
|
+
@associated_frames = Hash.new
|
74
|
+
@associated_studies = Hash.new
|
75
|
+
# Optional attributes:
|
76
|
+
@birth_date = options[:birth_date]
|
77
|
+
@sex = options[:sex]
|
78
|
+
# Register ourselves with the dataset:
|
79
|
+
@dataset.add_patient(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_patient)
|
86
|
+
other.send(:state) == state
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
alias_method :eql?, :==
|
91
|
+
|
92
|
+
# Adds a DICOM object to the patient, by adding it
|
93
|
+
# to an existing Study, or creating a new Study.
|
94
|
+
#
|
95
|
+
def add(dcm)
|
96
|
+
s = study(dcm.value(STUDY_UID))
|
97
|
+
if s
|
98
|
+
s.add(dcm)
|
99
|
+
else
|
100
|
+
add_study(Study.load(dcm, self))
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Adds a Frame to this Patient.
|
105
|
+
#
|
106
|
+
def add_frame(frame)
|
107
|
+
raise ArgumentError, "Invalid argument 'frame'. Expected Frame, got #{frame.class}." unless frame.is_a?(Frame)
|
108
|
+
# Do not add it again if the frame already belongs to this instance:
|
109
|
+
@frames << frame unless @associated_frames[frame.uid]
|
110
|
+
@associated_frames[frame.uid] = frame
|
111
|
+
end
|
112
|
+
|
113
|
+
# Adds a Study to this Patient.
|
114
|
+
#
|
115
|
+
def add_study(study)
|
116
|
+
raise ArgumentError, "Invalid argument 'study'. Expected Study, got #{study.class}." unless study.is_a?(Study)
|
117
|
+
# Do not add it again if the study already belongs to this instance:
|
118
|
+
@studies << study unless @associated_studies[study.uid]
|
119
|
+
@associated_studies[study.uid] = study
|
120
|
+
end
|
121
|
+
|
122
|
+
# Sets the birth_date attribute.
|
123
|
+
#
|
124
|
+
def birth_date=(value)
|
125
|
+
@birth_date = value && value.to_s
|
126
|
+
end
|
127
|
+
|
128
|
+
# Creates (and returns) a Frame instance added to this Patient.
|
129
|
+
#
|
130
|
+
# === Parameters
|
131
|
+
#
|
132
|
+
# * <tt>uid</tt> -- The Frame of Reference UID string.
|
133
|
+
# * <tt>indicator</tt> -- The Position Reference Indicator string. Defaults to nil.
|
134
|
+
#
|
135
|
+
def create_frame(uid, indicator=nil)
|
136
|
+
raise ArgumentError, "Invalid argument 'uid'. Expected String, got #{uid.class}." unless uid.is_a?(String)
|
137
|
+
raise ArgumentError, "Invalid argument 'indicator'. Expected String or nil, got #{indicator.class}." unless [String, NilClass].include?(indicator.class)
|
138
|
+
frame = Frame.new(uid, self, :indicator => indicator)
|
139
|
+
add_frame(frame)
|
140
|
+
@dataset.add_frame(frame)
|
141
|
+
return frame
|
142
|
+
end
|
143
|
+
|
144
|
+
# Returns the Frame instance mathcing the specified Frame Instance UID (if an argument is used).
|
145
|
+
# If a specified UID doesn't match, nil is returned.
|
146
|
+
# If no argument is passed, the first Frame instance associated with the Patient is returned.
|
147
|
+
#
|
148
|
+
# === Parameters
|
149
|
+
#
|
150
|
+
# * <tt>uid</tt> -- String. The value of the Frame Instance UID element.
|
151
|
+
#
|
152
|
+
def frame(*args)
|
153
|
+
raise ArgumentError, "Expected one or none arguments, got #{args.length}." unless [0, 1].include?(args.length)
|
154
|
+
if args.length == 1
|
155
|
+
raise ArgumentError, "Expected String (or nil), got #{args.first.class}." unless [String, NilClass].include?(args.first.class)
|
156
|
+
return @associated_frames[args.first]
|
157
|
+
else
|
158
|
+
# No argument used, therefore we return the first Frame instance:
|
159
|
+
return @frames.first
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Generates a Fixnum hash value for this instance.
|
164
|
+
#
|
165
|
+
def hash
|
166
|
+
state.hash
|
167
|
+
end
|
168
|
+
|
169
|
+
# Sets the id attribute.
|
170
|
+
#
|
171
|
+
def id=(value)
|
172
|
+
@id = value && value.to_s
|
173
|
+
end
|
174
|
+
|
175
|
+
# Sets the name attribute.
|
176
|
+
#
|
177
|
+
def name=(value)
|
178
|
+
@name = value && value.to_s
|
179
|
+
end
|
180
|
+
|
181
|
+
# Sets the sex attribute.
|
182
|
+
#
|
183
|
+
def sex=(value)
|
184
|
+
@sex = value && value.to_s
|
185
|
+
end
|
186
|
+
|
187
|
+
# Returns the Study instance mathcing the specified Study Instance UID (if an argument is used).
|
188
|
+
# If a specified UID doesn't match, nil is returned.
|
189
|
+
# If no argument is passed, the first Study instance associated with the Patient is returned.
|
190
|
+
#
|
191
|
+
# === Parameters
|
192
|
+
#
|
193
|
+
# * <tt>uid</tt> -- String. The value of the Study Instance UID element.
|
194
|
+
#
|
195
|
+
def study(*args)
|
196
|
+
raise ArgumentError, "Expected one or none arguments, got #{args.length}." unless [0, 1].include?(args.length)
|
197
|
+
if args.length == 1
|
198
|
+
raise ArgumentError, "Expected String (or nil), got #{args.first.class}." unless [String, NilClass].include?(args.first.class)
|
199
|
+
return @associated_studies[args.first]
|
200
|
+
else
|
201
|
+
# No argument used, therefore we return the first Study instance:
|
202
|
+
return @studies.first
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# Returns self.
|
207
|
+
#
|
208
|
+
def to_patient
|
209
|
+
self
|
210
|
+
end
|
211
|
+
|
212
|
+
# Returns the unique identifier string, which for this class is the Patient's ID.
|
213
|
+
#
|
214
|
+
def uid
|
215
|
+
return @id
|
216
|
+
end
|
217
|
+
|
218
|
+
|
219
|
+
private
|
220
|
+
|
221
|
+
|
222
|
+
# Returns the attributes of this instance in an array (for comparison purposes).
|
223
|
+
#
|
224
|
+
def state
|
225
|
+
[@birth_date, @frames, @id, @name, @sex, @studies]
|
226
|
+
end
|
227
|
+
|
228
|
+
end
|
229
|
+
end
|
@@ -0,0 +1,237 @@
|
|
1
|
+
module RTKIT
|
2
|
+
|
3
|
+
# A collection of methods for dealing with pixel data in both 2D images and 3D volumes.
|
4
|
+
#
|
5
|
+
# === Inheritance
|
6
|
+
#
|
7
|
+
# These methods are available to instances of the following classes:
|
8
|
+
# * Image
|
9
|
+
# * Dose
|
10
|
+
#
|
11
|
+
class PixelData
|
12
|
+
|
13
|
+
# Converts from two NArrays of image X & Y indices to physical coordinates X, Y & Z (in mm).
|
14
|
+
# The X, Y & Z coordinates are returned in three NArrays of equal size as the input index NArrays.
|
15
|
+
# The image coordinates are calculated using the direction cosines of the Image Orientation (Patient) element (0020,0037).
|
16
|
+
#
|
17
|
+
# === Notes
|
18
|
+
#
|
19
|
+
# * For details about Image orientation, refer to the DICOM standard: PS 3.3 C.7.6.2.1.1
|
20
|
+
#
|
21
|
+
def coordinates_from_indices(column_indices, row_indices)
|
22
|
+
raise ArgumentError, "Invalid argument 'column_indices'. Expected NArray, got #{column_indices.class}." unless column_indices.is_a?(NArray)
|
23
|
+
raise ArgumentError, "Invalid argument 'row_indices'. Expected NArray, got #{row_indices.class}." unless row_indices.is_a?(NArray)
|
24
|
+
raise ArgumentError, "Invalid arguments. Expected NArrays of equal length, got #{column_indices.length} and #{row_indices.length}." unless column_indices.length == row_indices.length
|
25
|
+
raise "Invalid attribute 'cosines'. Expected a 6 element Array, got #{cosines.class} #{cosines.length if cosines.is_a?(Array)}." unless cosines.is_a?(Array) && cosines.length == 6
|
26
|
+
raise "Invalid attribute 'pos_x'. Expected Float, got #{pos_x.class}." unless pos_x.is_a?(Float)
|
27
|
+
raise "Invalid attribute 'pos_y'. Expected Float, got #{pos_y.class}." unless pos_y.is_a?(Float)
|
28
|
+
raise "Invalid attribute 'pos_slice'. Expected Float, got #{pos_slice.class}." unless pos_slice.is_a?(Float)
|
29
|
+
raise "Invalid attribute 'col_spacing'. Expected Float, got #{col_spacing.class}." unless col_spacing.is_a?(Float)
|
30
|
+
raise "Invalid attribute 'row_spacing'. Expected Float, got #{row_spacing.class}." unless row_spacing.is_a?(Float)
|
31
|
+
# Convert indices integers to floats:
|
32
|
+
column_indices = column_indices.to_f
|
33
|
+
row_indices = row_indices.to_f
|
34
|
+
# Calculate the coordinates by multiplying indices with the direction cosines and applying the image offset:
|
35
|
+
x = pos_x + (column_indices * col_spacing * cosines[0]) + (row_indices * row_spacing * cosines[3])
|
36
|
+
y = pos_y + (column_indices * col_spacing * cosines[1]) + (row_indices * row_spacing * cosines[4])
|
37
|
+
z = pos_slice + (column_indices * col_spacing * cosines[2]) + (row_indices * row_spacing * cosines[5])
|
38
|
+
return x, y, z
|
39
|
+
end
|
40
|
+
|
41
|
+
# Converts from three (float) NArrays of X, Y & Z physical coordinates (in mm) to image slice indices X & Y.
|
42
|
+
# The X & Y indices are returned in two NArrays of equal size as the input coordinate NArrays.
|
43
|
+
# The image indices are calculated using the direction cosines of the Image Orientation (Patient) element (0020,0037).
|
44
|
+
#
|
45
|
+
# === Notes
|
46
|
+
#
|
47
|
+
# * For details about Image orientation, refer to the DICOM standard: PS 3.3 C.7.6.2.1.1
|
48
|
+
#
|
49
|
+
def coordinates_to_indices(x, y, z)
|
50
|
+
raise ArgumentError, "Invalid argument 'x'. Expected NArray, got #{x.class}." unless x.is_a?(NArray)
|
51
|
+
raise ArgumentError, "Invalid argument 'y'. Expected NArray, got #{y.class}." unless y.is_a?(NArray)
|
52
|
+
raise ArgumentError, "Invalid argument 'z'. Expected NArray, got #{z.class}." unless z.is_a?(NArray)
|
53
|
+
raise ArgumentError, "Invalid arguments. Expected NArrays of equal length, got #{x.length}, #{y.length} and #{z.length}." unless [x.length, y.length, z.length].uniq.length == 1
|
54
|
+
raise "Invalid attribute 'cosines'. Expected a 6 element Array, got #{cosines.class} #{cosines.length if cosines.is_a?(Array)}." unless cosines.is_a?(Array) && cosines.length == 6
|
55
|
+
raise "Invalid attribute 'pos_x'. Expected Float, got #{pos_x.class}." unless pos_x.is_a?(Float)
|
56
|
+
raise "Invalid attribute 'pos_y'. Expected Float, got #{pos_y.class}." unless pos_y.is_a?(Float)
|
57
|
+
raise "Invalid attribute 'pos_slice'. Expected Float, got #{pos_slice.class}." unless pos_slice.is_a?(Float)
|
58
|
+
raise "Invalid attribute 'col_spacing'. Expected Float, got #{col_spacing.class}." unless col_spacing.is_a?(Float)
|
59
|
+
raise "Invalid attribute 'row_spacing'. Expected Float, got #{row_spacing.class}." unless row_spacing.is_a?(Float)
|
60
|
+
# Calculate the indices by multiplying coordinates with the direction cosines and applying the image offset:
|
61
|
+
column_indices = ((x-pos_x)/col_spacing*cosines[0] + (y-pos_y)/col_spacing*cosines[1] + (z-pos_slice)/col_spacing*cosines[2]).round
|
62
|
+
row_indices = ((x-pos_x)/row_spacing*cosines[3] + (y-pos_y)/row_spacing*cosines[4] + (z-pos_slice)/row_spacing*cosines[5]).round
|
63
|
+
return column_indices, row_indices
|
64
|
+
end
|
65
|
+
|
66
|
+
# Fills the provided image array with lines of a specified value, based on two vectors of column and row indices.
|
67
|
+
# The image is expected to be a (two-dimensional) NArray.
|
68
|
+
# Returns the processed image array.
|
69
|
+
#
|
70
|
+
def draw_lines(column_indices, row_indices, image, value)
|
71
|
+
raise ArgumentError, "Invalid argument 'column_indices'. Expected Array, got #{column_indices.class}." unless column_indices.is_a?(Array)
|
72
|
+
raise ArgumentError, "Invalid argument 'row_indices'. Expected Array, got #{row_indices.class}." unless row_indices.is_a?(Array)
|
73
|
+
raise ArgumentError, "Invalid arguments. Expected Arrays of equal length, got #{column_indices.length}, #{row_indices.length}." unless column_indices.length == row_indices.length
|
74
|
+
raise ArgumentError, "Invalid argument 'image'. Expected NArray, got #{image.class}." unless image.is_a?(NArray)
|
75
|
+
raise ArgumentError, "Invalid number of dimensions for argument 'image'. Expected 2, got #{image.shape.length}." unless image.shape.length == 2
|
76
|
+
raise ArgumentError, "Invalid argument 'value'. Expected Integer, got #{value.class}." unless value.is_a?(Integer)
|
77
|
+
column_indices.each_index do |i|
|
78
|
+
image = draw_line(column_indices[i-1], column_indices[i], row_indices[i-1], row_indices[i], image, value)
|
79
|
+
end
|
80
|
+
return image
|
81
|
+
end
|
82
|
+
|
83
|
+
# Iterative, queue based flood fill algorithm.
|
84
|
+
# Replaces all pixels of a specific value that are contained by pixels of different value.
|
85
|
+
# The replacement value along with the starting coordinates are passed as parameters to this method.
|
86
|
+
# It seems a recursive method is not suited for Ruby due to its limited stack space (a problem in general for scripting languages).
|
87
|
+
#
|
88
|
+
def flood_fill(col, row, image, fill_value)
|
89
|
+
existing_value = image[col, row]
|
90
|
+
queue = Array.new
|
91
|
+
queue.push([col, row])
|
92
|
+
until queue.empty?
|
93
|
+
col, row = queue.shift
|
94
|
+
if image[col, row] == existing_value
|
95
|
+
west_col, west_row = ff_find_border(col, row, existing_value, :west, image)
|
96
|
+
east_col, east_row = ff_find_border(col, row, existing_value, :east, image)
|
97
|
+
# Fill the line between the two border pixels (i.e. not touching the border pixels):
|
98
|
+
image[west_col..east_col, row] = fill_value
|
99
|
+
q = west_col
|
100
|
+
while q <= east_col
|
101
|
+
[:north, :south].each do |direction|
|
102
|
+
same_col, next_row = ff_neighbour(q, row, direction)
|
103
|
+
begin
|
104
|
+
queue.push([q, next_row]) if image[q, next_row] == existing_value
|
105
|
+
rescue
|
106
|
+
# Out of bounds. Do nothing.
|
107
|
+
end
|
108
|
+
end
|
109
|
+
q, same_row = ff_neighbour(q, row, :east)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
return image
|
114
|
+
end
|
115
|
+
|
116
|
+
# Converts general image indices to specific column and row indices based on the
|
117
|
+
# provided image indices and the number of columns in the image.
|
118
|
+
#
|
119
|
+
def indices_general_to_specific(indices, n_cols)
|
120
|
+
if indices.is_a?(Array)
|
121
|
+
row_indices = indices.collect{|i| i/n_cols}
|
122
|
+
column_indices = [indices, row_indices].transpose.collect{|i| i[0] - i[1] * n_cols}
|
123
|
+
else
|
124
|
+
# Assume Fixnum or NArray:
|
125
|
+
row_indices = indices/n_cols # Values are automatically rounded down.
|
126
|
+
column_indices = indices-row_indices*n_cols
|
127
|
+
end
|
128
|
+
return column_indices, row_indices
|
129
|
+
end
|
130
|
+
|
131
|
+
# Converts specific x and y indices to general image indices based on the provided specific indices and x size of the NArray image.
|
132
|
+
#
|
133
|
+
def indices_specific_to_general(column_indices, row_indices, n_cols)
|
134
|
+
if column_indices.is_a?(Array)
|
135
|
+
indices = Array.new
|
136
|
+
column_indices.each_index {|i| indices << column_indices[i] + row_indices[i] * n_cols}
|
137
|
+
return indices
|
138
|
+
else
|
139
|
+
# Assume Fixnum or NArray:
|
140
|
+
return column_indices + row_indices * n_cols
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# A convenience method for printing image information.
|
145
|
+
# NB! This has been used only for debugging, and will soon be removed.
|
146
|
+
#
|
147
|
+
def print_img(narr=@narray)
|
148
|
+
puts "Image dimensions: #{@columns}*#{@rows}"
|
149
|
+
narr.shape[0].times do |i|
|
150
|
+
puts narr[true, i].to_a.to_s
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
|
158
|
+
# Draws a single line in the (NArray) image matrix based on a start- and an end-point.
|
159
|
+
# The method uses an iterative Bresenham Line Algorithm.
|
160
|
+
# Returns the processed image array.
|
161
|
+
#
|
162
|
+
def draw_line(x0, x1, y0, y1, image, value)
|
163
|
+
steep = ((y1-y0).abs) > ((x1-x0).abs)
|
164
|
+
if steep
|
165
|
+
x0,y0 = y0,x0
|
166
|
+
x1,y1 = y1,x1
|
167
|
+
end
|
168
|
+
if x0 > x1
|
169
|
+
x0,x1 = x1,x0
|
170
|
+
y0,y1 = y1,y0
|
171
|
+
end
|
172
|
+
deltax = x1-x0
|
173
|
+
deltay = (y1-y0).abs
|
174
|
+
error = (deltax / 2).to_i
|
175
|
+
y = y0
|
176
|
+
ystep = nil
|
177
|
+
if y0 < y1
|
178
|
+
ystep = 1
|
179
|
+
else
|
180
|
+
ystep = -1
|
181
|
+
end
|
182
|
+
for x in x0..x1
|
183
|
+
if steep
|
184
|
+
begin
|
185
|
+
image[y,x] = value # (switching variables)
|
186
|
+
rescue
|
187
|
+
# Our line has gone outside the image. Do nothing for now, but the proper thing to do would be to at least return some status boolean indicating that this has occured.
|
188
|
+
end
|
189
|
+
else
|
190
|
+
begin
|
191
|
+
image[x,y] = value
|
192
|
+
rescue
|
193
|
+
# Our line has gone outside the image.
|
194
|
+
end
|
195
|
+
end
|
196
|
+
error -= deltay
|
197
|
+
if error < 0
|
198
|
+
y += ystep
|
199
|
+
error += deltax
|
200
|
+
end
|
201
|
+
end
|
202
|
+
return image
|
203
|
+
end
|
204
|
+
|
205
|
+
# Searches left and right to find the 'border' in a row of pixels the image array.
|
206
|
+
# Returns a column and row index. Used by the flood_fill() method.
|
207
|
+
#
|
208
|
+
def ff_find_border(col, row, existing_value, direction, image)
|
209
|
+
next_col, next_row = ff_neighbour(col, row, direction)
|
210
|
+
begin
|
211
|
+
while image[next_col, next_row] == existing_value
|
212
|
+
col, row = next_col, next_row
|
213
|
+
next_col, next_row = ff_neighbour(col, row, direction)
|
214
|
+
end
|
215
|
+
rescue
|
216
|
+
# Out of bounds. Do nothing.
|
217
|
+
end
|
218
|
+
col = 0 if col < 1
|
219
|
+
row = 0 if row < 1
|
220
|
+
return col, row
|
221
|
+
end
|
222
|
+
|
223
|
+
# Returns the neighbour index based on the specified direction.
|
224
|
+
# Used by the flood_fill() method and its dependency; the ff_find_border() method.
|
225
|
+
# :east => to the right when looking at an array image printout on the screen
|
226
|
+
#
|
227
|
+
def ff_neighbour(col, row, direction)
|
228
|
+
case direction
|
229
|
+
when :north then return col, row-1
|
230
|
+
when :south then return col, row+1
|
231
|
+
when :east then return col+1, row
|
232
|
+
when :west then return col-1, row
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|
237
|
+
end
|