Class: RTKIT::Plane

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

Overview

A Plane describes a flat, two-dimensional surface. A Plane can be defined in several ways, e.g. by:

  • A point and a normal vector.

  • A point and two vectors lying on it.

  • 3 non-colinear points.

We will describe the plane in the form of a plane equation:

ax + by +cz +d = 0

Notes

Relations

Since an image slice is located in a specific plane, the Plane class may be used to relate instances of such classes.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(a, b, c) ⇒ Plane

Creates a new Plane instance.

Parameters

  • a – Float. The a parameter of the plane equation.

  • b – Float. The b parameter of the plane equation.

  • c – Float. The c parameter of the plane equation.

Raises:

  • (ArgumentError)


77
78
79
80
81
82
83
84
85
# File 'lib/rtkit/plane.rb', line 77

def initialize(a, b, c)
  raise ArgumentError, "Invalid argument 'a'. Expected Float, got #{a.class}." unless a.is_a?(Float)
  raise ArgumentError, "Invalid argument 'b'. Expected Float, got #{b.class}." unless b.is_a?(Float)
  raise ArgumentError, "Invalid argument 'c'. Expected Float, got #{c.class}." unless c.is_a?(Float)
  @a = a
  @b = b
  @c = c
  @d = Plane.d
end

Instance Attribute Details

#aObject (readonly)

The a parameter of the plane equation.



23
24
25
# File 'lib/rtkit/plane.rb', line 23

def a
  @a
end

#bObject (readonly)

The b parameter of the plane equation.



25
26
27
# File 'lib/rtkit/plane.rb', line 25

def b
  @b
end

#cObject (readonly)

The c parameter of the plane equation.



27
28
29
# File 'lib/rtkit/plane.rb', line 27

def c
  @c
end

#dObject (readonly)

The d parameter of the plane equation.



29
30
31
# File 'lib/rtkit/plane.rb', line 29

def d
  @d
end

Class Method Details

.calculate(c1, c2, c3) ⇒ Object

Calculates a plane equation from the 3 specified coordinates. Returns a Plane instance.

Raises:

  • (ArgumentError)


34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/rtkit/plane.rb', line 34

def self.calculate(c1, c2, c3)
  raise ArgumentError, "Invalid argument 'c1'. Expected Coordinate, got #{c1.class}" unless c1.is_a?(Coordinate)
  raise ArgumentError, "Invalid argument 'c2'. Expected Coordinate, got #{c2.class}" unless c2.is_a?(Coordinate)
  raise ArgumentError, "Invalid argument 'c3'. Expected Coordinate, got #{c3.class}" unless c3.is_a?(Coordinate)
  x1, y1, z1 = c1.x.to_r, c1.y.to_r, c1.z.to_r
  x2, y2, z2 = c2.x.to_r, c2.y.to_r, c2.z.to_r
  x3, y3, z3 = c3.x.to_r, c3.y.to_r, c3.z.to_r
  raise ArgumentError, "Got at least two Coordinates that are equal. Expected unique Coordinates."  unless [[x1, y1, z1], [x2, y2, z2], [x3, y3, z3]].uniq.length == 3
  det = Matrix.rows([[x1, y1, z1], [x2, y2, z2], [x3, y3, z3]]).determinant
  if det == 0
    # Haven't experienced this case yet. Just raise an error to avoid unexpected behaviour.
    raise "The determinant was zero (which means the plane passes through the origin). Not able to calculate variables: a,b,c"
    #puts "Determinant was zero. Plane passes through origin. Find direction cosines instead?"
  else
    det = det.to_f
    # Find parameters a,b,c.
    a_m = Matrix.rows([[1, y1, z1], [1, y2, z2], [1, y3, z3]])
    b_m = Matrix.rows([[x1, 1, z1], [x2, 1, z2], [x3, 1, z3]])
    c_m = Matrix.rows([[x1, y1, 1], [x2, y2, 1], [x3, y3, 1]])
    d = Plane.d
    a = -d / det * a_m.determinant
    b = -d / det * b_m.determinant
    c = -d / det * c_m.determinant
    return self.new(a, b, c)
  end
end

.dObject

The custom plane parameter d: This constant can be equal to any non-zero number. Returns the float value chosen in this implementation: 500.0



65
66
67
# File 'lib/rtkit/plane.rb', line 65

def self.d
  500.0
end

Instance Method Details

#==(other) ⇒ Object Also known as: eql?

Returns true if the argument is an instance with attributes equal to self.



89
90
91
92
93
# File 'lib/rtkit/plane.rb', line 89

def ==(other)
  if other.respond_to?(:to_plane)
    other.send(:state) == state
  end
end

#hashObject

Generates a Fixnum hash value for this instance.



99
100
101
# File 'lib/rtkit/plane.rb', line 99

def hash
  state.hash
end

#match(planes) ⇒ Object

Compares the Plane instance with an array of planes, and returns the index of the Plane who’s Plane equation is closest to the plane equation of self. Returns nil if no suitable match is found.

Raises:

  • (ArgumentError)


107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/rtkit/plane.rb', line 107

def match(planes)
  raise ArgumentError, "Invalid argument 'planes'. Expected Array, got #{planes.class}." unless planes.is_a?(Array)
  raise ArgumentError, "Invalid argument 'planes'. Expected Array containing only Planes, got #{planes.collect{|p| p.class}.uniq}." unless planes.collect{|p| p.class}.uniq == [Plane]
  # I don't really have a feeling for what a reasonable threshold should be here. Setting it at 0.01.
  # (So far, matched planes have been observed having a deviation in the order of 10e-5 to 10e-7,
  # while some obviously different planes have had values in the range of 8-21)
  deviation_threshold = 0.01
  # Since the 'd' parameter is just a constant selected by us when determining plane equations,
  # the comparison is carried out against the a, b & c parameters.
  # Register deviation for each parameter for each plane:
  a_deviations = NArray.float(planes.length)
  b_deviations = NArray.float(planes.length)
  c_deviations = NArray.float(planes.length)
  planes.each_index do |i|
    # Calculate absolute deviation for each of the three parameters:
    a_deviations[i] = (planes[i].a - @a).abs
    b_deviations[i] = (planes[i].b - @b).abs
    c_deviations[i] = (planes[i].c - @c).abs
  end
  # Compare the deviations of each parameter with the average deviation for that parameter,
  # taking care to adress the case where all deviations for a certain parameter may be 0:
  a_relatives = a_deviations.mean == 0 ? a_deviations : a_deviations / a_deviations.mean
  b_relatives = b_deviations.mean == 0 ? b_deviations : b_deviations / b_deviations.mean
  c_relatives = c_deviations.mean == 0 ? c_deviations : c_deviations / c_deviations.mean
  # Sum the relative deviations for each parameter, and find the index with the lowest summed relative deviation:
  deviations = NArray.float(planes.length)
  planes.each_index do |i|
    deviations[i] = a_relatives[i] + b_relatives[i] + c_relatives[i]
  end
  index = (deviations.eq deviations.min).where[0]
  deviation = a_deviations[index] + b_deviations[index] + c_deviations[index]
  index = nil if deviation > deviation_threshold
  return index
end

#to_planeObject

Returns self.



144
145
146
# File 'lib/rtkit/plane.rb', line 144

def to_plane
  self
end

#to_sObject

Converts the Plane instance to a readable string (containing the parameters a, b & c).



150
151
152
# File 'lib/rtkit/plane.rb', line 150

def to_s
  return "a: #{@a.round(2)}  b: #{@b.round(2)} c: #{@c.round(2)}"
end