Class: RTKIT::PixelData

Inherits:
Object
  • Object
show all
Defined in:
lib/rtkit/pixel_data.rb

Overview

A collection of methods for dealing with pixel data in both 2D images and 3D volumes.

Inheritance

These methods are available to instances of the following classes:

  • Image

  • Dose

Direct Known Subclasses

BinImage, BinVolume, Image

Instance Method Summary collapse

Instance Method Details

#coordinates_from_indices(column_indices, row_indices) ⇒ Object

Converts from two NArrays of image X & Y indices to physical coordinates X, Y & Z (in mm). The X, Y & Z coordinates are returned in three NArrays of equal size as the input index NArrays. The image coordinates are calculated using the direction cosines of the Image Orientation (Patient) element (0020,0037).

Notes

  • For details about Image orientation, refer to the DICOM standard: PS 3.3 C.7.6.2.1.1

Raises:

  • (ArgumentError)


21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/rtkit/pixel_data.rb', line 21

def coordinates_from_indices(column_indices, row_indices)
  raise ArgumentError, "Invalid argument 'column_indices'. Expected NArray, got #{column_indices.class}." unless column_indices.is_a?(NArray)
  raise ArgumentError, "Invalid argument 'row_indices'. Expected NArray, got #{row_indices.class}." unless row_indices.is_a?(NArray)
  raise ArgumentError, "Invalid arguments. Expected NArrays of equal length, got #{column_indices.length} and #{row_indices.length}." unless column_indices.length == row_indices.length
  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
  raise "Invalid attribute 'pos_x'. Expected Float, got #{pos_x.class}." unless pos_x.is_a?(Float)
  raise "Invalid attribute 'pos_y'. Expected Float, got #{pos_y.class}." unless pos_y.is_a?(Float)
  raise "Invalid attribute 'pos_slice'. Expected Float, got #{pos_slice.class}." unless pos_slice.is_a?(Float)
  raise "Invalid attribute 'col_spacing'. Expected Float, got #{col_spacing.class}." unless col_spacing.is_a?(Float)
  raise "Invalid attribute 'row_spacing'. Expected Float, got #{row_spacing.class}." unless row_spacing.is_a?(Float)
  # Convert indices integers to floats:
  column_indices = column_indices.to_f
  row_indices = row_indices.to_f
  # Calculate the coordinates by multiplying indices with the direction cosines and applying the image offset:
  x = pos_x + (column_indices * col_spacing * cosines[0]) + (row_indices * row_spacing * cosines[3])
  y = pos_y + (column_indices * col_spacing * cosines[1]) + (row_indices * row_spacing * cosines[4])
  z = pos_slice + (column_indices * col_spacing * cosines[2]) + (row_indices * row_spacing * cosines[5])
  return x, y, z
end

#coordinates_to_indices(x, y, z) ⇒ Object

Converts from three (float) NArrays of X, Y & Z physical coordinates (in mm) to image slice indices X & Y. The X & Y indices are returned in two NArrays of equal size as the input coordinate NArrays. The image indices are calculated using the direction cosines of the Image Orientation (Patient) element (0020,0037).

Notes

  • For details about Image orientation, refer to the DICOM standard: PS 3.3 C.7.6.2.1.1

Raises:

  • (ArgumentError)


49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/rtkit/pixel_data.rb', line 49

def coordinates_to_indices(x, y, z)
  raise ArgumentError, "Invalid argument 'x'. Expected NArray, got #{x.class}." unless x.is_a?(NArray)
  raise ArgumentError, "Invalid argument 'y'. Expected NArray, got #{y.class}." unless y.is_a?(NArray)
  raise ArgumentError, "Invalid argument 'z'. Expected NArray, got #{z.class}." unless z.is_a?(NArray)
  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
  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
  raise "Invalid attribute 'pos_x'. Expected Float, got #{pos_x.class}." unless pos_x.is_a?(Float)
  raise "Invalid attribute 'pos_y'. Expected Float, got #{pos_y.class}." unless pos_y.is_a?(Float)
  raise "Invalid attribute 'pos_slice'. Expected Float, got #{pos_slice.class}." unless pos_slice.is_a?(Float)
  raise "Invalid attribute 'col_spacing'. Expected Float, got #{col_spacing.class}." unless col_spacing.is_a?(Float)
  raise "Invalid attribute 'row_spacing'. Expected Float, got #{row_spacing.class}." unless row_spacing.is_a?(Float)
  # Calculate the indices by multiplying coordinates with the direction cosines and applying the image offset:
  column_indices = ((x-pos_x)/col_spacing*cosines[0] + (y-pos_y)/col_spacing*cosines[1] + (z-pos_slice)/col_spacing*cosines[2]).round
  row_indices = ((x-pos_x)/row_spacing*cosines[3] + (y-pos_y)/row_spacing*cosines[4] + (z-pos_slice)/row_spacing*cosines[5]).round
  return column_indices, row_indices
end

#draw_lines(column_indices, row_indices, image, value) ⇒ Object

Fills the provided image array with lines of a specified value, based on two vectors of column and row indices. The image is expected to be a (two-dimensional) NArray. Returns the processed image array.

Raises:

  • (ArgumentError)


70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/rtkit/pixel_data.rb', line 70

def draw_lines(column_indices, row_indices, image, value)
  raise ArgumentError, "Invalid argument 'column_indices'. Expected Array, got #{column_indices.class}." unless column_indices.is_a?(Array)
  raise ArgumentError, "Invalid argument 'row_indices'. Expected Array, got #{row_indices.class}." unless row_indices.is_a?(Array)
  raise ArgumentError, "Invalid arguments. Expected Arrays of equal length, got #{column_indices.length}, #{row_indices.length}." unless column_indices.length == row_indices.length
  raise ArgumentError, "Invalid argument 'image'. Expected NArray, got #{image.class}." unless image.is_a?(NArray)
  raise ArgumentError, "Invalid number of dimensions for argument 'image'. Expected 2, got #{image.shape.length}." unless image.shape.length == 2
  raise ArgumentError, "Invalid argument 'value'. Expected Integer, got #{value.class}." unless value.is_a?(Integer)
  column_indices.each_index do |i|
    image = draw_line(column_indices[i-1], column_indices[i], row_indices[i-1], row_indices[i], image, value)
  end
  return image
end

#flood_fill(col, row, image, fill_value) ⇒ Object

Iterative, queue based flood fill algorithm. Replaces all pixels of a specific value that are contained by pixels of different value. The replacement value along with the starting coordinates are passed as parameters to this method. It seems a recursive method is not suited for Ruby due to its limited stack space (a problem in general for scripting languages).



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/rtkit/pixel_data.rb', line 88

def flood_fill(col, row, image, fill_value)
  existing_value = image[col, row]
  queue = Array.new
  queue.push([col, row])
  until queue.empty?
    col, row = queue.shift
    if image[col, row] == existing_value
      west_col, west_row = ff_find_border(col, row, existing_value, :west, image)
      east_col, east_row = ff_find_border(col, row, existing_value, :east, image)
      # Fill the line between the two border pixels (i.e. not touching the border pixels):
      image[west_col..east_col, row] = fill_value
      q = west_col
      while q <= east_col
        [:north, :south].each do |direction|
          same_col, next_row = ff_neighbour(q, row, direction)
          begin
            queue.push([q, next_row]) if image[q, next_row] == existing_value
          rescue
            # Out of bounds. Do nothing.
          end
        end
        q, same_row = ff_neighbour(q, row, :east)
      end
    end
  end
  return image
end

#indices_general_to_specific(indices, n_cols) ⇒ Object

Converts general image indices to specific column and row indices based on the provided image indices and the number of columns in the image.



119
120
121
122
123
124
125
126
127
128
129
# File 'lib/rtkit/pixel_data.rb', line 119

def indices_general_to_specific(indices, n_cols)
  if indices.is_a?(Array)
    row_indices = indices.collect{|i| i/n_cols}
    column_indices = [indices, row_indices].transpose.collect{|i| i[0] - i[1] * n_cols}
  else
    # Assume Fixnum or NArray:
    row_indices = indices/n_cols # Values are automatically rounded down.
    column_indices = indices-row_indices*n_cols
  end
  return column_indices, row_indices
end

#indices_specific_to_general(column_indices, row_indices, n_cols) ⇒ Object

Converts specific x and y indices to general image indices based on the provided specific indices and x size of the NArray image.



133
134
135
136
137
138
139
140
141
142
# File 'lib/rtkit/pixel_data.rb', line 133

def indices_specific_to_general(column_indices, row_indices, n_cols)
  if column_indices.is_a?(Array)
    indices = Array.new
    column_indices.each_index {|i| indices << column_indices[i] + row_indices[i] * n_cols}
    return indices
  else
    # Assume Fixnum or NArray:
    return column_indices + row_indices * n_cols
  end
end

A convenience method for printing image information. NB! This has been used only for debugging, and will soon be removed.



147
148
149
150
151
152
# File 'lib/rtkit/pixel_data.rb', line 147

def print_img(narr=@narray)
  puts "Image dimensions: #{@columns}*#{@rows}"
  narr.shape[0].times do |i|
    puts narr[true, i].to_a.to_s
  end
end