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,578 @@
1
+ module RTKIT
2
+
3
+ # Contains the DICOM data and methods related to a binary image.
4
+ #
5
+ # === Inheritance
6
+ #
7
+ # * As the BinImage class inherits from the PixelData class, all PixelData methods are available to instances of BinImage.
8
+ #
9
+ class BinImage < PixelData
10
+
11
+ # The BinImage's Image reference.
12
+ attr_reader :image
13
+ # The binary numerical image array.
14
+ attr_reader :narray
15
+ # A narray containing pixel indices.
16
+ attr_reader :narray_indices
17
+
18
+ # Creates a new BinImage instance from an array of contours.
19
+ # The BinVolume is typically defined from a ROI delineation against an image series,
20
+ # but it may also be applied to an rtdose 'image' series.
21
+ # Returns the BinVolume instance.
22
+ #
23
+ # === Parameters
24
+ #
25
+ # * <tt>contours</tt> -- An array of contours from which to fill in a binary image.
26
+ # * <tt>image</tt> -- The image that this BinImage instance will be based on.
27
+ # * <tt>bin_volume</tt> -- The BinVolume instance that this bin_image belongs to.
28
+ #
29
+ def self.from_contours(contours, image, bin_volume)
30
+ raise ArgumentError, "Invalid argument 'contours'. Expected Array, got #{contours.class}." unless contours.is_a?(Array)
31
+ raise ArgumentError, "Invalid argument 'image'. Expected Image, got #{image.class}." unless image.is_a?(Image)
32
+ raise ArgumentError, "Invalid argument 'bin_volume'. Expected BinVolume, got #{bin_volume.class}." unless bin_volume.is_a?(BinVolume)
33
+ # Create the narray to be used:
34
+ narr = NArray.byte(image.columns, image.rows)
35
+ # Create the BinImage instance:
36
+ bi = self.new(narr, image)
37
+ # Delineate and fill for each contour:
38
+ contours.each do |contour|
39
+ x, y, z = contour.coords
40
+ bi.add(image.binary_image(x, y, z))
41
+ end
42
+ bin_volume.add(bi)
43
+ return bi
44
+ end
45
+
46
+ # Creates a new BinImage instance.
47
+ #
48
+ # === Parameters
49
+ #
50
+ # * <tt>narray</tt> -- A binary, two-dimensional NArray.
51
+ # * <tt>image</tt> -- The Image instance that this BinImage is associated with.
52
+ #
53
+ def initialize(narray, image)
54
+ raise ArgumentError, "Invalid argument 'narray'. Expected NArray, got #{narray.class}." unless narray.is_a?(NArray)
55
+ raise ArgumentError, "Invalid argument 'image'. Expected Image, got #{image.class}." unless image.is_a?(Image)
56
+ raise ArgumentError, "Invalid argument 'narray'. Expected two-dimensional NArray, got #{narray.shape.length} dimensions." unless narray.shape.length == 2
57
+ raise ArgumentError, "Invalid argument 'narray'. Expected NArray of element size 1 byte, got #{narray.element_size} bytes (per element)." unless narray.element_size == 1
58
+ raise ArgumentError, "Invalid argument 'narray'. Expected binary NArray with max value 1, got #{narray.max} as max." if narray.max > 1
59
+ self.narray = narray
60
+ @image = image
61
+ end
62
+
63
+ # Returns true if the argument is an instance with attributes equal to self.
64
+ #
65
+ def ==(other)
66
+ if other.respond_to?(:to_bin_image)
67
+ other.send(:state) == state
68
+ end
69
+ end
70
+
71
+ alias_method :eql?, :==
72
+
73
+ # Adds a binary image array to the image array of this instance.
74
+ # Any segmented pixels in the new array (value = 1), is added (value set eql to 1) to the instance array.
75
+ #
76
+ def add(pixels)
77
+ raise ArgumentError, "Invalid argument 'pixels'. Expected NArray, got #{pixels.class}." unless pixels.is_a?(NArray)
78
+ raise ArgumentError, "Invalid argument 'pixels'. Expected NArray of element size 1 byte, got #{pixels.element_size} bytes (per element)." unless pixels.element_size == 1
79
+ raise ArgumentError, "Invalid argument 'pixels'. Expected binary NArray with max value 1, got #{pixels.max} as max." if pixels.max > 1
80
+ raise ArgumentError, "Invalid argument 'pixels'. Expected NArray to have same dimension as the instance array. Got #{pixels.shape}, expected #{@narray.shape}." unless pixels.shape == @narray.shape
81
+ @narray[(pixels > 0).where] = 1
82
+ end
83
+
84
+ # Calculates the area defined by true/false (1/0) pixels.
85
+ # By default, the area of the true pixels are returned.
86
+ # Returns a float value, in units of millimeters squared.
87
+ #
88
+ # === Parameters
89
+ #
90
+ # * <tt>type</tt> -- Boolean. Pixel type of interest.
91
+ #
92
+ def area(type=true)
93
+ if type
94
+ number = (@narray.eq 1).where.length
95
+ else
96
+ number = (@narray.eq 0).where.length
97
+ end
98
+ # Total area is number of pixels times the area per pixel:
99
+ return number * @image.pixel_area
100
+ end
101
+
102
+ # Returns the col_spacing attribute from the Image reference.
103
+ # This attribute defines the physical distance (in millimeters) between columns in the pixel data (i.e. horisontal spacing).
104
+ #
105
+ def col_spacing
106
+ return @image.col_spacing
107
+ end
108
+
109
+ # Returns the number of columns in the binary array.
110
+ #
111
+ def columns
112
+ return @narray.shape[0]
113
+ end
114
+
115
+ # Applies the contour indices of this instance to an empty image (2D NArray)
116
+ # to create a 'contour image'.
117
+ # Each separate contour is indicated by individual integers (e.g. 1,2,3 etc).
118
+ #
119
+ def contour_image
120
+ img = NArray.byte(columns, rows)
121
+ contour_indices.each_with_index do |contour, i|
122
+ img[contour.indices] = i + 1
123
+ end
124
+ return img
125
+ end
126
+
127
+ # Extracts the contour indices of the (filled) structures contained in the BinImage,
128
+ # by performing a contour tracing algorithm on the binary image.
129
+ # Returns an array filled with contour Selection instances, with length
130
+ # equal to the number of separated structures in the image.
131
+ #
132
+ # === Notes
133
+ #
134
+ # * The contours are established using a contour tracing algorithm called "Radial Sweep":
135
+ # * http://www.imageprocessingplace.com/downloads_V3/root_downloads/tutorials/contour_tracing_Abeer_George_Ghuneim/ray.html
136
+ #
137
+ # === Restrictions
138
+ #
139
+ # * Does not detect inner contour of hollow structures (holes).
140
+ #
141
+ def contour_indices
142
+ # Create the array to be returned:
143
+ contours = Array.new
144
+ # Initialize the contour extraction process if indicated:
145
+ if @narray.segmented?
146
+ # Initialize some variables used by the contour algorithm:
147
+ initialize_contour_reorder_structures unless @reorder
148
+ # The contour algorithm needs the image to be padded with a border of zero-pixels:
149
+ original_image = @narray
150
+ padded_image = NArray.byte(columns + 2, rows + 2)
151
+ padded_image[1..-2, 1..-2] = @narray
152
+ # Temporarily replace our instance image with the padded image:
153
+ self.narray = padded_image
154
+ # Get the contours:
155
+ padded_contours = extract_contours
156
+ # Convert from padded indices to proper indices:
157
+ padded_contours.each do |padded_contour|
158
+ padded_contour.shift_and_crop(-1, -1)
159
+ contours << padded_contour
160
+ end
161
+ # Restore the instance image:
162
+ self.narray = original_image
163
+ end
164
+ return contours
165
+ end
166
+
167
+ # Returns the cosines attribute from the Image reference.
168
+ #
169
+ def cosines
170
+ return @image.cosines
171
+ end
172
+
173
+ # Generates a Fixnum hash value for this instance.
174
+ #
175
+ def hash
176
+ state.hash
177
+ end
178
+
179
+ # Sets a new binary array for this BinImage instance.
180
+ #
181
+ def narray=(image)
182
+ raise ArgumentError, "Invalid argument 'image'. Expected NArray, got #{image.class}." unless image.is_a?(NArray)
183
+ raise ArgumentError, "Invalid argument 'image'. Expected two-dimensional NArray, got #{image.shape.length} dimensions." unless image.shape.length == 2
184
+ raise ArgumentError, "Invalid argument 'image'. Expected NArray of element size 1 byte, got #{image.element_size} bytes (per element)." unless image.element_size == 1
185
+ raise ArgumentError, "Invalid argument 'image'. Expected binary NArray with max value 1, got #{image.max} as max." if image.max > 1
186
+ @narray = image
187
+ # Create a corresponding array of the image indices (used in image processing):
188
+ @narray_indices = NArray.int(columns, rows).indgen!
189
+ end
190
+
191
+ # Returns the pos_slice attribute from the Image reference.
192
+ # This attribute defines the physical position (in millimeters) of the image slice.
193
+ # Returns nil if there is no Image reference.
194
+ #
195
+ def pos_slice
196
+ return @image ? @image.pos_slice : nil
197
+ end
198
+
199
+ # Returns the pos_x attribute from the Image reference.
200
+ # This attribute defines the physical position (in millimeters) of the first (left) column in the pixel data.
201
+ #
202
+ def pos_x
203
+ return @image.pos_x
204
+ end
205
+
206
+ # Returns the pos_y attribute from the Image reference.
207
+ # This attribute defines the physical position (in millimeters) of the first (top) row in the pixel data.
208
+ #
209
+ def pos_y
210
+ return @image.pos_y
211
+ end
212
+
213
+ # Returns the row_spacing attribute from the Image reference.
214
+ # This attribute defines the physical distance (in millimeters) between rows in the pixel data (i.e. vertical spacing).
215
+ #
216
+ def row_spacing
217
+ return @image.row_spacing
218
+ end
219
+
220
+ # Returns the number of rows in the binary array.
221
+ #
222
+ def rows
223
+ return @narray.shape[1]
224
+ end
225
+
226
+ # Creates a Selection containing all 'segmented' indices of this instance, i.e.
227
+ # indices of all pixels with a value of 1.
228
+ # Returns the Selection instance.
229
+ #
230
+ def selection
231
+ s = Selection.new(self)
232
+ s.add_indices((@narray.eq 1).where)
233
+ return s
234
+ end
235
+
236
+ # Returns self.
237
+ #
238
+ def to_bin_image
239
+ self
240
+ end
241
+
242
+ # Converts the BinImage instance to a single image BinVolume instance.
243
+ #
244
+ # === Parameters
245
+ #
246
+ # * <tt>series</tt> -- The image series (e.g. ImageSeries or DoseVolume) which forms the reference data of the BinVolume.
247
+ # * <tt>source</tt> -- The object which is the source of the binary (segmented) data (i.e. ROI or Dose/Hounsfield threshold).
248
+ #
249
+ def to_bin_volume(series, source=nil)
250
+ bin_volume = BinVolume.new(series, :images => [self], :source => source)
251
+ end
252
+
253
+ # Creates an array of Contour instances from the segmentation of this BinImage.
254
+ # Returns the array of Contours.
255
+ # Returns an empty array if no Contours are created (empty BinImage).
256
+ #
257
+ # === Parameters
258
+ #
259
+ # * <tt>slice</tt> -- A Slice instance which the Contours will be connected to.
260
+ #
261
+ def to_contours(slice)
262
+ raise ArgumentError, "Invalid argument 'slice. Expected Slice, got #{slice.class}." unless slice.is_a?(Slice)
263
+ contours = Array.new
264
+ # Iterate the extracted collection of contour indices and convert to Contour instances:
265
+ contour_indices.each do |contour|
266
+ # Convert column and row indices to X, Y and Z coordinates:
267
+ x, y, z = coordinates_from_indices(NArray.to_na(contour.columns), NArray.to_na(contour.rows))
268
+ # Convert NArray to Array and round the coordinate floats:
269
+ x = x.to_a.collect {|f| f.round(1)}
270
+ y = y.to_a.collect {|f| f.round(1)}
271
+ z = z.to_a.collect {|f| f.round(3)}
272
+ contours << Contour.create_from_coordinates(x, y, z, slice)
273
+ end
274
+ return contours
275
+ end
276
+
277
+ # Dumps the BinImage instance to a DObject.
278
+ # This is achieved by copying the Elements of the DICOM object of the Image instance referenced by this BinImage,
279
+ # and replacing its pixel data with the NArray of this instance.
280
+ # Returns the DObject instance.
281
+ #
282
+ def to_dcm
283
+ # Use the original DICOM object as a starting point (keeping all non-sequence elements):
284
+ # Note: Something like dcm.dup doesn't work here because that only performs a shallow copy on the DObject instance.
285
+ dcm = DICOM::DObject.new
286
+ @image.dcm.each_element do |element|
287
+ # A bit of a hack to regenerate the DICOM elements:
288
+ begin
289
+ if element.value
290
+ # ATM this fails for tags with integer values converted to a backslash-separated string:
291
+ DICOM::Element.new(element.tag, element.value, :parent => dcm)
292
+ else
293
+ # Transfer the binary content as long as it is not the pixel data string:
294
+ DICOM::Element.new(element.tag, element.bin, :encoded => true, :parent => dcm)
295
+ end
296
+ rescue
297
+ DICOM::Element.new(element.tag, element.value.split("\\").collect {|val| val.to_i}, :parent => dcm) if element.value
298
+ end
299
+ end
300
+ dcm.delete_group('0002')
301
+ # Format the DICOM image ensure good contrast amongst the binary pixel values:
302
+ # Window Center:
303
+ DICOM::Element.new('0028,1050', '128', :parent => dcm)
304
+ # Window Width:
305
+ DICOM::Element.new('0028,1051', '256', :parent => dcm)
306
+ # Rescale Intercept:
307
+ DICOM::Element.new('0028,1052', '0', :parent => dcm)
308
+ # Rescale Slope:
309
+ DICOM::Element.new('0028,1053', '1', :parent => dcm)
310
+ # Pixel data:
311
+ dcm.pixels = @narray*255
312
+ return dcm
313
+ end
314
+
315
+ # Creates a Slice instance from the segmentation of this BinImage.
316
+ # This method also creates and connects any child structures as indicated in the item (e.g. Contours).
317
+ # Returns the Slice instance.
318
+ #
319
+ # === Parameters
320
+ #
321
+ # * <tt>roi</tt> -- A ROI instance which the Slice will be connected to.
322
+ #
323
+ def to_slice(roi)
324
+ raise ArgumentError, "Invalid argument 'roi'. Expected ROI, got #{roi.class}." unless roi.is_a?(ROI)
325
+ # Create the Slice:
326
+ s = Slice.new(@image.uid, roi)
327
+ # Create Contours:
328
+ to_contours(s)
329
+ return s
330
+ end
331
+
332
+ # Writes the BinImage to a DICOM file given by the specified file string.
333
+ #
334
+ def write(path)
335
+ dcm = to_dcm
336
+ dcm.write(path)
337
+ end
338
+
339
+
340
+ private
341
+
342
+
343
+ # Determines a set of pixel indices which enclose the structure.
344
+ #
345
+ # === Notes
346
+ #
347
+ # * Uses Roman Khudeevs algorithm: A New Flood-Fill Algorithm for Closed Contour
348
+ # * https://docs.google.com/viewer?a=v&q=cache:UZ6bo7pXRoIJ:file.lw23.com/file1/01611214.pdf+flood+fill+from+contour+coordinate&hl=no&gl=no&pid=bl&srcid=ADGEEShV4gbKYYq8cDagjT7poT677cIL44K0QW8SR0ODanFy-CD1CHEQi2RvHF8MND7_PXPGYRJMJAcMJO-NEXkM-vU4iA2rNljVetbzuARWuHtKLJKMTNjd3vaDWrIeSU4rKLCVwvff&sig=AHIEtbSAnH6fp584c0_Krv298n-tgpNcJw&pli=1
349
+ #
350
+ def external_contour
351
+ start_index = (@narray > 0).where[0] - 1
352
+ s_col, s_row = indices_general_to_specific(start_index, columns)
353
+ col, row = s_col, s_row
354
+ row_indices = Array.new(1, row)
355
+ col_indices = Array.new(1, col)
356
+ last_dir = :north # on first step, pretend we came from the south (going north)
357
+ directions = {
358
+ :north => {:dir => [:east, :north, :west, :south], :col => [1, 0, -1, 0], :row => [0, -1, 0, 1]},
359
+ :east => {:dir => [:south, :east, :north, :west], :col => [0, 1, 0, -1], :row => [1, 0, -1, 0]},
360
+ :south => {:dir => [:west, :south, :east, :north], :col => [-1, 0, 1, 0], :row => [0, 1, 0, -1]},
361
+ :west => {:dir => [:north, :west, :south, :east], :col => [0, -1, 0, 1], :row => [-1, 0, 1, 0]},
362
+ }
363
+ loop = true
364
+ while loop do
365
+ # Probe the neighbourhood pixels in a CCW order:
366
+ map = directions[last_dir]
367
+ 4.times do |i|
368
+ # Find the first 'free' (zero) pixel, and make that index
369
+ # the next pixel of our external contour:
370
+ if @narray[col + map[:col][i], row + map[:row][i]] == 0
371
+ last_dir = map[:dir][i]
372
+ col = col + map[:col][i]
373
+ row = row + map[:row][i]
374
+ col_indices << col
375
+ row_indices << row
376
+ break
377
+ end
378
+ end
379
+ loop = false if col == s_col and row == s_row
380
+ end
381
+ return Selection.create_from_array(indices_specific_to_general(col_indices, row_indices, columns), self)
382
+ end
383
+
384
+ # This is a recursive method which extracts a contour, determines all pixels
385
+ # belonging to this contour, removes them from the binary image, then
386
+ # repeats collecting contours until there are no more pixels left.
387
+ # Returns an array of contour selections.
388
+ #
389
+ def extract_contours
390
+ contours = Array.new
391
+ if @narray.segmented?
392
+ # Get contours:
393
+ corners, continuous = extract_single_contour
394
+ # If we dont get at least 3 indices, there is no area to fill.
395
+ if continuous.indices.length < 3
396
+ # In this case we remove the pixels and do not record the contour indices:
397
+ roi = continuous
398
+ else
399
+ # Record the indices and get all indices of the structure:
400
+ contours << corners
401
+ # Flood fill the image to determine all pixels contained by the contoured structure:
402
+ roi = roi_indices(continuous)
403
+ # A precaution:
404
+ raise "Unexpected result: #{roi.indices.length}. Raising an error to avoid an infinite recursion!" if roi.indices.length < 3
405
+ end
406
+ # Reset the pixels belonging to the contoured structure from the image:
407
+ @narray[roi.indices] = 0
408
+ # Repeat with the 'cleaned' image to get any additional contours present:
409
+ contours += extract_contours
410
+ end
411
+ return contours
412
+ end
413
+
414
+ # Returns contour indices (Selection) from the first (if any) structure
415
+ # found in the binary image of this instance.
416
+ #
417
+ # FIXME: For now, a rather simple corner detection algorithm is integrated and used.
418
+ # At some stage this could be replaced/supplemented with a proper ('lossy') corner detection algorithm.
419
+ #
420
+ def extract_single_contour
421
+ # Set up an array to keep track of the pixels belonging to the current contour being analyzed by the algorithm:
422
+ current_indices = Array.new
423
+ # Also keep track of the direction we 'arrived from' (in addition to the pixel position):
424
+ contour_directions = Array.new
425
+ # First step of contour algorithm: Identify a border pixel which will be our start pixel.
426
+ # Traditionally this is achieved by scanning the image, row by row, left to right, until a foreground pixel is found.
427
+ # Instead of scanning, this implementation will extract all foreground indices and chose the first index as our start pixel.
428
+ indices = (@narray > 0).where
429
+ if indices.length > 0
430
+ current_indices << indices[0]
431
+ # Generally we will store the direction we came from along with the pixel position - but not for the first pixel.
432
+ # Specific indices for the first pixel:
433
+ p_col, p_row = indices_general_to_specific(current_indices.first, columns)
434
+ # Set up variables for the initial run of the contour algorithm loop:
435
+ arrived_from = :west
436
+ direction_from_corner = nil
437
+ continue = true
438
+ while continue do
439
+ # Radially sweep the 8 pixels surrounding the start pixel, until a foreground pixel is found.
440
+ # We do this by extracting a 3*3 array centered on our position. Based on the direction to the previous pixel,
441
+ # we then extract the neighbour pixels in such a way the pixel where we came from in the previous step is first in our vector.
442
+ local_pixels = @narray[(p_col-1)..(p_col+1), (p_row-1)..(p_row+1)].flatten
443
+ local_indices = @narray_indices[(p_col-1)..(p_col+1), (p_row-1)..(p_row+1)].flatten
444
+ neighbour_pixels = local_pixels[@reorder[arrived_from]]
445
+ neighbour_indices = local_indices[@reorder[arrived_from]]
446
+ neighbour_relative_indices = @relative_indices[@reorder[arrived_from]]
447
+ # The next border pixel is then the first foreground pixel in the extracted vector.
448
+ local_foreground = (neighbour_pixels > 0).where
449
+ if local_foreground.length > 0
450
+ # We identified another border pixel.
451
+ first_foreground_index = local_foreground[0]
452
+ current_pixel_index = neighbour_indices[first_foreground_index]
453
+ # Stopping criterion: If current pixel equals the second pixel, and the previous pixel equals the first pixel,
454
+ # then we can be absolutely sure that we have made a full contour, regardless of the connectivity of the border.
455
+ if current_pixel_index == current_indices[1] and current_indices.last == current_indices.first
456
+ # We have re-located the original start pixels. Remove the duplicate last element and abort the search.
457
+ current_indices.pop
458
+ #contour_directions.pop
459
+ continue = false
460
+ else
461
+ # Extract x and y index as well as the direction we arrived from:
462
+ p_col, p_row = indices_general_to_specific(current_pixel_index, columns)
463
+ arrived_from = @arrived_from_directions[neighbour_relative_indices[first_foreground_index]]
464
+ # Store pixel and continue the search, using the newly identified border pixel in the next step.
465
+ current_indices << current_pixel_index
466
+ contour_directions << arrived_from
467
+ end
468
+ else
469
+ # No foreground pixel was found, search finished.
470
+ continue = false
471
+ end
472
+ end
473
+ # Before reducing to corner indices, make a copy of the full set of indices:
474
+ all_indices = Selection.create_from_array(current_indices, self)
475
+ # We only want corner points: Remove all the indices that are between corner points:
476
+ img_contour_original = current_indices.dup
477
+ current_indices = Array.new(1, img_contour_original.first)
478
+ img_contour_original.delete_at(0)
479
+ original_direction = contour_directions.first
480
+ contour_directions.delete_at(0)
481
+ img_contour_original.each_index do |i|
482
+ if contour_directions[i] != original_direction
483
+ # Store pixel and set new direction:
484
+ current_indices << img_contour_original[i]
485
+ original_direction = contour_directions[i]
486
+ end
487
+ end
488
+ corner_indices = Selection.create_from_array(current_indices, self)
489
+ end
490
+ return corner_indices, all_indices
491
+ end
492
+
493
+ # Initializes a couple of instance variables containing directional information, which are used by the contour algorithm.
494
+ #
495
+ # The directional vectors of indices are used for extracting a vector of neighbour pixels from a 3*3 pixel array,
496
+ # where the resulting vector contains 7 neighbour pixels (the previous pixel is removed), in a clockwise order,
497
+ # where the first pixel is the neighbour pixel that is next to the previous pixel, following a clockwise rotation.
498
+ #
499
+ def initialize_contour_reorder_structures
500
+ @reorder = Hash.new
501
+ @reorder[:west] = NArray[0,1,2,5,8,7,6,3]
502
+ @reorder[:nw] = NArray[1,2,5,8,7,6,3,0]
503
+ @reorder[:north] = NArray[2,5,8,7,6,3,0,1]
504
+ @reorder[:ne] = NArray[5,8,7,6,3,0,1,2]
505
+ @reorder[:east] = NArray[8,7,6,3,0,1,2,5]
506
+ @reorder[:se] = NArray[7,6,3,0,1,2,5,8]
507
+ @reorder[:south] = NArray[6,3,0,1,2,5,8,7]
508
+ @reorder[:sw] = NArray[3,0,1,2,5,8,7,6]
509
+ @arrived_from_directions = Hash.new
510
+ @arrived_from_directions[0] = :se
511
+ @arrived_from_directions[1] = :south
512
+ @arrived_from_directions[2] = :sw
513
+ @arrived_from_directions[3] = :east
514
+ @arrived_from_directions[5] = :west
515
+ @arrived_from_directions[6] = :ne
516
+ @arrived_from_directions[7] = :north
517
+ @arrived_from_directions[8] = :nw
518
+ # Set up the index of pixels in a neighborhood image extract:
519
+ @relative_indices = NArray.int(3, 3).indgen!
520
+ end
521
+
522
+ # Determines all pixel indices which belongs to the specified (continuous) contour indices.
523
+ #
524
+ # === Notes
525
+ #
526
+ # This is achieved by applying an external contour (around the original contour),
527
+ # and identifying the indices that are enclosed by this external contour. This identification
528
+ # is carried out by scanning line by line, and marking pixels which lies between two external
529
+ # contour points.
530
+ #
531
+ # * Uses Roman Khudeevs algorithm: A New Flood-Fill Algorithm for Closed Contour
532
+ # * https://docs.google.com/viewer?a=v&q=cache:UZ6bo7pXRoIJ:file.lw23.com/file1/01611214.pdf+flood+fill+from+contour+coordinate&hl=no&gl=no&pid=bl&srcid=ADGEEShV4gbKYYq8cDagjT7poT677cIL44K0QW8SR0ODanFy-CD1CHEQi2RvHF8MND7_PXPGYRJMJAcMJO-NEXkM-vU4iA2rNljVetbzuARWuHtKLJKMTNjd3vaDWrIeSU4rKLCVwvff&sig=AHIEtbSAnH6fp584c0_Krv298n-tgpNcJw&pli=1
533
+ #
534
+ def roi_indices(contour)
535
+ raise ArgumentError, "Invalid argument 'contour'. Expected Selection, got #{contour.class}." unless contour.is_a?(Selection)
536
+ ext_value = 3
537
+ int_value = 2
538
+ roi_value = 1
539
+ # Determine external contour:
540
+ ext_contour = external_contour
541
+ row_indices = ext_contour.rows
542
+ # Apply the two contours to an image:
543
+ img = NArray.byte(columns, rows)
544
+ img[ext_contour.indices] = ext_value
545
+ img[contour.indices] = int_value
546
+ # Iterate row by row where the contour is defined:
547
+ (row_indices.min..row_indices.max).each do |row_index|
548
+ img_vector = img[true, row_index]
549
+ row_ext_indices = (img_vector.gt 0).where
550
+ # Iterate the column:
551
+ ext_left = nil
552
+ int_found = nil
553
+ (row_ext_indices.min..row_ext_indices.max).each do |col_index|
554
+ if img_vector[col_index] == ext_value and !int_found
555
+ ext_left = col_index
556
+ elsif img_vector[col_index] == int_value
557
+ int_found = true
558
+ elsif img_vector[col_index] == ext_value and int_found
559
+ # We have identified a span of pixels which belong to the internal contour:
560
+ img[(ext_left+1)..(col_index-1), row_index] = roi_value
561
+ # Reset our indicators:
562
+ ext_left = col_index
563
+ int_found = nil
564
+ end
565
+ end
566
+ end
567
+ # Extract and return our roi indices:
568
+ return Selection.create_from_array((img.eq roi_value).where.to_a, self)
569
+ end
570
+
571
+ # Returns the attributes of this instance in an array (for comparison purposes).
572
+ #
573
+ def state
574
+ [@narray]
575
+ end
576
+
577
+ end
578
+ end