class Point < Struct.new(:x, :y) # Generates a random point around (0,0) with radius = 1 def self.random radius = rand theta = 360 * rand Point.new(radius*Math.cos(theta), radius*Math.sin(theta)) end # Returns true if x is close to y as defined by eps def self.close_to(x, y, eps = 1e-9) return (x-y).abs < eps end def self.rad2deg(a) return (a * 180.0) / Math::PI end def normal_left Point.new(self.y, -self.x) end # Returns the point at the center of this point # and the other_point def center(other_point) Point.new((self.x + other_point.x)/2.0, (self.y + other_point.y)/2.0) end # Computes the distance between self and other_point def distance(other_point) ((self.x - other_point.x) ** 2 + (self.y - other_point.y) ** 2) ** 0.5 end def self.inf_line_intersection(base1, v1, base2, v2) if (Point.close_to(v1.x, 0.0) && Point.close_to(v2.y, 0.0)) return Point.new(base1.x, base2.y) end if (Point.close_to(v1.y, 0.0) && Point.close_to(v2.x, 0.0)) return Point.new(base2.x, base1.y) end m1 = !Point.close_to(v1.x, 0.0) ? v1.y / v1.x : 0.0 m2 = !Point.close_to(v2.x, 0.0) ? v2.y / v2.x : 0.0 if Point.close_to(m1, m2) return nil end c1 = base1.y - m1 * base1.x c2 = base2.y - m2 * base2.x ix = (c2 - c1) / (m1 - m2) iy = m1 * ix + c1 if Point.close_to(v1.x, 0.0) return Point.new(base1.x, base1.x * m2 + c2) end if Point.close_to(v2.x, 0.0) return Point.new(base2.x, base2.x * m1 + c1) end return Point.new(ix, iy) end # compute the polar angle of this point def polar theta = 0.0 if Point.close_to(self.x, 0) if self.y > 0 theta = 90.0 elsif self.y < 0 theta = 270.0 end else theta = Point.rad2deg(Math.atan(self.y/self.x)) if self.x < 0.0 theta += 180.0 end if theta < 0.0 theta += 360.0 end end theta end # Computes the angle formed by p1 - self - p2 def angle_between(p1, p2) vect_p1 = p1 - self vect_p2 = p2 - self theta = vect_p1.polar - vect_p2.polar theta += 360.0 if theta < 0.0 theta = 360.0 - theta if theta > 180.0 return theta end def ==(other_point) return Point.close_to(self.x, other_point.x) && Point.close_to(self.y, other_point.y) end # We define sort order of Points based on x coordinate, then y coordinate def <=>(other_point) ret = self.x <=> other_point.x if ret == 0 ret = self.y <=> other_point.y end ret end # Subtract other_point from self def -(other_point) Point.new(self.x - other_point.x, self.y - other_point.y) end # Add other_point to self def +(other_point) Point.new(self.x + other_point.x, self.y + other_point.y) end # Scale this point by the provided factor def scale(factor) Point.new(self.x * factor, self.y * factor) end # Pretty print a point def to_s "(%5.4f, %5.4f)" % [x, y] end end class Circle < Struct.new(:center, :radius) # Returns true if this Circle contains the given Point def contains?(point) point.distance(self.center) < self.radius || (point.distance(self.center) - self.radius).abs < 1e-9 end def to_s "{center:%s, radius:%5.4f}}" % [center.to_s, radius] end end require 'set' class Solver < Struct.new(:mec) def initialize(debug) @points = SortedSet.new @debug = debug end # add a point to the solver and recalcluate the minimum enclosing # circle if the point lies outise the current MEC. def add_point(point) puts "adding p: #{point}" if @debug @points << point if self.mec && self.mec.center unless self.mec.contains?(point) iterate end else iterate end end # Tests if a point is left|on|right of a infinite line # return >0 for p2 left of line through p0 and p1 # return = 0 for p2 on the line # return <0 for p2 right of the line def direction(p0, p1, p2) return (p0.x-p1.x)*(p2.y-p1.y) - (p2.x-p1.x)*(p0.y-p1.y) end # Computes the set of points that represents the # convex hull of the Solver's @points def convex_hull a = @points.to_a left = a.first a = a[1..-1] right = a.pop puts "left = #{left} right = #{right} a = #{a.map{|p| p.to_s}.join(", ")}" if @debug upper = [] lower = [] # Partition remaining points into upper and lower buckets a.each do |p| dir = direction(left, right, p) puts "p = #{p} dir = #{dir}" if @debug if dir < 0 upper << p else lower << p end end puts "upper = #{upper.map{|p| p.to_s}.join(', ')}" if @debug puts "lower = #{lower.map{|p| p.to_s}.join(', ')}" if @debug upper_hull = half_hull(left, right, upper, -1) lower_hull = half_hull(left, right, lower, 1) puts "upper_hull = #{upper_hull.map{|p| p.to_s}.join(', ')}" if @debug puts "lower_hull = #{lower_hull.map{|p| p.to_s}.join(', ')}" if @debug (upper_hull + lower_hull).uniq end # Computs a half hull def half_hull(left, right, input, factor) input = input.dup input << right half = [] half << left puts "input: #{input.map{|p| p.to_s}.join(', ')}" if @debug input.each do |p| half << p puts "half: #{half.map{|p| p.to_s}.join(', ')}" if @debug while half.size >= 3 dir = factor * direction(half[-3], half[-1], half[-2]) if dir <= 0 half.delete_at(half.size-2) else break end end end half end # Recalculates the MEC after the addition of a new point def iterate self.mec ||= Circle.new # Handle degenerate cases first if @points.size == 1 self.mec.center = @points.to_a[0] self.mec.radius = 0 elsif @points.size == 2 a = @points.to_a self.mec.center = a[0].center(a[1]) self.mec.radius = a[0].distance(self.mec.center) else puts "points = #{@points.map{|p| p.to_s}.join(", ")}" if @debug a = convex_hull puts "convex hull = #{a.map{|p| p.to_s}.join(', ')}" if @debug point_a = a[0] point_c = a[1] while (true) point_b = nil best_theta = 180.0; for point in @points if point != point_a && point != point_c theta_abc = point.angle_between(point_a, point_c) puts "a: #{point_a} c: #{point_c} p: #{point} best_theta: #{best_theta} theta_abc: #{theta_abc}" if @debug if theta_abc < best_theta point_b = point best_theta = theta_abc end end end puts "Found best theta: #{best_theta}, a = #{point_a}, b = #{point_b}, c = #{point_c}" if @debug if best_theta >= 90.0 || point_b.nil? self.mec.center = point_a.center(point_c) self.mec.radius = point_a.distance(self.mec.center) return self.mec end ang_bca = point_c.angle_between(point_b, point_a) ang_cab = point_a.angle_between(point_c, point_b) puts "ang_bca = #{ang_bca}, ang_cab = #{ang_cab}" if @debug if ang_bca > 90.0 point_c = point_b elsif ang_cab <= 90.0 puts "breaking ..." if @debug break else point_a = point_b end end ch1 = (point_b - point_a).scale(0.5) ch2 = (point_c - point_a).scale(0.5) n1 = ch1.normal_left n2 = ch2.normal_left ch1 = point_a + ch1 ch2 = point_a + ch2 self.mec.center = Point.inf_line_intersection(ch1, n1, ch2, n2) self.mec.radius = self.mec.center.distance(point_a) end self.mec end end def distance(p1, p2) return p1.distance(p2) end # Use algorithm invented by Pr. Chrystal (1885) # Port of code in a Java Applet found at # http://www.personal.kent.edu/~rmuhamma/Compgeometry/MyCG/CG-Applets/Center/centercli.htm # MEC Is determined by the Convex Hull of the set of points # takes array of Point objects # returns a Circle object def encircle(points, debug = false) solver = Solver.new(debug) points.each { |p| solver.add_point(p) } puts "SOLVED: mec: #{solver.mec}" if debug return solver.mec end # Generate an array of random Point objects in the unit circle # with center (0,0) def generate_samples(n) (1..n).map { Point.random } end # Creates an image using rmagick showing the points and the MEC and # saves it as filename def draw(points, circle, filename) require 'rubygems' require 'RMagick' # Change these to control the output dim_x = 500 # width of the box to draw the circle in dim_y = 500 # height of the box to draw the circle in o_x = dim_x / 2 # x-origin on the graph o_y = dim_y / 2 # y-origin on the graph # Create a canvas and drawing context using rmagick canvas = Magick::Image.new(dim_x, dim_y, Magick::HatchFill.new('white', 'lightcyan2', dim_y / 20)) gc = Magick::Draw.new gc.stroke_width(1) # Draw axes gc.stroke('black') gc.line(0, o_x, dim_y, o_x) gc.line(o_y, 0, o_y, dim_x) # Draw the MEC gc.stroke('red') gc.fill_opacity(0) cx = o_x + circle.center.x * dim_x / 2 cy = o_y - circle.center.y * dim_y / 2 cy_edge = cy - circle.radius * dim_y / 2 gc.circle(cx, cy, cx, cy_edge) gc.fill_opacity(1) gc.fill('red') gc.circle(cx, cy, cx, cy+3) gc.fill_opacity(0) # Draw the MEC radius gc.stroke('green') cx_edge = cx - circle.radius * dim_x / 2 * (1 / (2 ** 0.5)) cy_edge = cy + circle.radius * dim_y / 2 * (1 / (2 ** 0.5)) gc.line(cx, cy, cx_edge, cy_edge) # Annotate the radius gc.stroke('darkgreen').fill('darkgreen') gc.text(cx_edge - 5, cy_edge + 5, "R:%5.4f" % circle.radius) # Annotate the center gc.stroke('darkgreen').fill('darkgreen') gc.text(cx + 5, cy, "(%3.2f, %3.2f)" % [circle.center.x, circle.center.y]) # Draw the points points.each do |point| gc.stroke('blue') gc.fill('blue') gc.fill_opacity(1) px = o_x + point.x * dim_x / 2 py = o_y - point.y * dim_y / 2 gc.circle(px, py, px, py+3) # Annotate the point gc.stroke('black').fill('black') gc.text(px + 5, py, "(%2.2f, %2.2f)" % [point.x, point.y]) end # draw the graphics context on the canvas and dump it to a file gc.draw(canvas) canvas.write(filename) end # For debugging, checks the solution and if it doesn't work, creates the image # and reruns encircle in debug mode for the failed solution def check_solution(points, circle) points.each do |point| if !circle.contains?(point) encircle(points, true) draw(points, circle, 'circle.png') puts "point #{point} (#{point.distance(circle.center)}) is outside of circle #{circle}" raise "BAD SOLUTION: point #{point} (#{point.distance(circle.center)}) is outside of circle #{circle}" end end return true end if __FILE__ == $0 srand if ARGV[0] && ARGV[0] == 'draw' n = ARGV[1] ? ARGV[1].to_i : 15 points = generate_samples(n) circle = encircle(points) draw(points, circle, 'circle.png') puts "SOLUTION: #{circle}" elsif ARGV[0] && ARGV[0] == 'benchmark' require 'benchmark' Benchmark.bm(15) do |x| 5.times do |i| n = 10 ** i x.report("points: #{n}") do 25.times do points = generate_samples(n) circle = encircle(points) check_solution(points, circle) end end end end elsif ARGV[0] && ARGV[0] == 'once' n = ARGV[1] ? ARGV[1].to_i : 15 points = generate_samples(n) puts encircle(points) else puts "Usage: ruby mec.rb [draw [num_points]] | [benchmark] | [once [num_points]]" puts " once: Runs the solution once and puts the circle" puts " num_points defaults to 15 if not specified." puts " draw: draws num_points points and the minimum enclosing circle to the file circle.png" puts " num_points defaults to 15 if not specified." puts "benchmark: benchmarks the algorithm" end end