rtkit 0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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