191 Mathematical Image Generator

Description

This week’s quiz is about the generation of images based on mathematical functions. The red, green, and blue values at each point in the image will each be determined by a separate function based on the coordinates at that position.

Here are some functions to get started:

Math.sin(Math::PI * ?)
Math.cos(Math::PI * ?)
(? + ?)/2
? * ?
(?) ** 3

The question marks represent the parameters to the functions (x, y, or another function). The x and y values range between 0 and 1: the proportion of the current position to the total width or height.

Feel free to add your own functions, but keep in mind that they work best when the inputs and output are between -1 and 1.

Example Output

depth: 3
red: Math.sin(Math::PI * Math.cos(Math::PI * y * x))
green: Math.sin(Math::PI * (Math.sin(Math::PI * y)) ** 3)
blue: (Math.cos(Math::PI * Math.cos(Math::PI * y))) ** 3

The depth is how many layers of functions to combine. The bottom layer is always x or y. Depth 3 is where things begin to get a little bit interesting, but 5 and higher is much more exciting.

Performance can be an issue with so many computations per pixel, therefore the solution that performs quickest on a 1600x1200 image with depth of 7 will be the winner of this quiz!

Good luck!

UPDATE

Function depth is more of a tree depth in this circumstance. Some functions take two parameters, some take one parameter. So depth of 0 would be either x or y. Depth 1 would be any of the following:

(x + x)/2
(x + y)/2
sin(PI * x)
sin(PI * y)
cos(PI * x)
...

Depth 2 could be:

(sin(PI * x) + x * y)/2

Part of the difficulty in this quiz is composing functions of different arity. Hopefully this additional explanation clears things up a little.

Summary

This week’s quiz was about creating interesting mathematical images using randomly generated formulae.

The overall architecture of the application is straightforward:

Lars’s solution provides a simple shell script which randomly generates formulae of depth 4 and saves image out as a png file. Lars’s solution also makes it easy to add custom functions to the generator by pushing them on to an array.

Alex Fenton provides a desktop application (using wxRuby) that allows for custom input of formulae and can save the images in multiple formats. If you are interested in learning wxRuby do examine Alex’s solution as it provides a good example of a wxRuby application and is well commented.

The two solutions provided slightly different conversions from function results to pixel colors. Alex’s treated everything <= 0 as 0, while Lars mapped -1 to 0 and 1 to 255, with 0 being 128 (on an 8-bit RBG scale). This causes some images to appear wildly different, observe:

image with zero at midrange image with zero and less cutoff

One aspect of the challenge was to generate the images quickly. Here are the results of the submissions using ruby-prof to benchmark.

ruby 1.8.6 (2007-09-24 patchlevel 111) [i486-linux]
width: 256
height: 256
r: cos(sin((sin(-x * PI)) ** 3 * PI) * PI)
g: (sin(((-y + y) / 2) ** 3 * PI)) ** 3
b: ((cos(-x * y * PI) + cos(cos(y * PI) * PI)) / 2 + sin(x * PI) * cos(y * PI) * x * -x * sin(x * PI)) / 2

Lars Haugseth
Total: 6.860000
 %self     total     self     wait    child    calls  name
 44.61      6.86     3.06     0.00     3.80      256  Integer#upto-1 (ruby_runtime:0}
 18.95      1.30     1.30     0.00     0.00  1179648  Float#* (ruby_runtime:0}
  6.71      0.46     0.46     0.00     0.00   327680  Math#cos (ruby_runtime:0}
  5.25      0.36     0.36     0.00     0.00   393216  Float#+ (ruby_runtime:0}
  4.96      0.34     0.34     0.00     0.00   327680  Math#sin (ruby_runtime:0}
  4.23      0.29     0.29     0.00     0.00   262400  Float#/ (ruby_runtime:0}
  3.50      0.24     0.24     0.00     0.00   196608  Float#** (ruby_runtime:0}
  3.21      0.22     0.22     0.00     0.00   196608  String#<< (ruby_runtime:0}
  2.92      0.20     0.20     0.00     0.00   262144  Float#-@ (ruby_runtime:0}
  2.33      0.16     0.16     0.00     0.00   196608  Integer#chr (ruby_runtime:0}
  1.90      0.13     0.13     0.00     0.00   196608  Float#to_i (ruby_runtime:0}
  1.46      0.10     0.10     0.00     0.00    65792  Fixnum#to_f (ruby_runtime:0}
  0.00      6.86     0.00     0.00     6.86        1  Integer#upto (ruby_runtime:0}
  0.00      6.86     0.00     0.00     6.86        1  Kernel#eval (ruby_runtime:0}
  0.00      0.00     0.00     0.00     0.00      257  Fixnum#- (ruby_runtime:0}
  0.00      6.86     0.00     0.00     6.86        1  ImageGenerator#generate_pixels (solution.rb:48}


Alex Fenton
Total: 7.800000
 %self     total     self     wait    child    calls  name
 41.15      5.90     3.21     0.00     2.69   196608  Proc#call (ruby_runtime:0}
 15.13      1.18     1.18     0.00     0.00  1179648  Float#* (ruby_runtime:0}
 12.18      7.79     0.95     0.00     6.84      256  Integer#upto (ruby_runtime:0}
  6.28      0.62     0.49     0.00     0.13    65536  Array#pack (ruby_runtime:0}
  4.23      0.33     0.33     0.00     0.00   262400  Float#/ (ruby_runtime:0}
  4.10      0.32     0.32     0.00     0.00   327680  Math#cos (ruby_runtime:0}
  3.85      0.30     0.30     0.00     0.00   327680  Math#sin (ruby_runtime:0}
  3.72      0.29     0.29     0.00     0.00   262144  Float#-@ (ruby_runtime:0}
  3.33      0.26     0.26     0.00     0.00   196608  Float#** (ruby_runtime:0}
  2.31      0.18     0.18     0.00     0.00   196608  Float#+ (ruby_runtime:0}
  1.67      0.13     0.13     0.00     0.00   196608  Float#to_int (ruby_runtime:0}
  1.15      0.09     0.09     0.00     0.00    65794  Fixnum#to_f (ruby_runtime:0}
  0.90      0.07     0.07     0.00     0.00    65536  String#<< (ruby_runtime:0}
  0.00      7.80     0.00     0.00     7.80        1  Integer#downto (ruby_runtime:0}
  0.00      7.80     0.00     0.00     7.80        1  MathsDrawing#make_image (solution.rb:74}
  0.00      0.00     0.00     0.00     0.00      257  Fixnum#- (ruby_runtime:0}

When I first ran the benchmark I noticed a discrepancy in the number of times that various Float and Math methods were called. It turns out that Alex Fenton’s solution was computing one extra row of pixels. I corrected this for the results. Here are the sections of code that were used in the benchmark:

# Lars Haugseth
@pixels = ""
eval <<"."
  0.upto(@height - 1) do |y_pos|
    y = (y_pos.to_f / @height)
    0.upto(@width - 1) do |x_pos|
      x = (x_pos.to_f / @width)
      @pixels << (127.99 + 127.99 * #{@red}).to_i.chr
      @pixels << (127.99 + 127.99 * #{@green}).to_i.chr
      @pixels << (127.99 + 127.99 * #{@blue}).to_i.chr
    end
  end
.

# Alex Fenton
# The string holding raw image data
data = ''
x_factor = size_x.to_f
y_factor = size_y.to_f

# Input values from the range 0 to 1, with origin in the bottom left
(size_y - 1).downto(0) do | y |
  the_y = y.to_f / y_factor
  0.upto(size_x - 1) do | x |
    the_x = x.to_f / x_factor
    red   = @red.call(the_x, the_y) * 255
    green = @green.call(the_x, the_y) * 255
    blue  = @blue.call(the_x, the_y) * 255
    data << [red, green, blue].pack("CCC")
  end
end

After running several several iterations Lars’s code was always about one second faster. I believe this is due to the single eval in place of the multiple (196,608) Proc#calls. Note that this is on Ruby 1.8.6 and may have different results on 1.9.1.

No one got really crazy with performance and created a lookup table for trigonometric functions, but that may be one way to save cycles, especially when creating much larger images. Another possible technique would be to use ruby2c, or something like it, to create a C method instead of using Procs or eval.

Let us bask in the beauty of these majestic behemoths, these gentle giants of the deep:

Christmas box magic

spironator

winters delight

nebulogue

crystal cavern of the paraboloids

beam and bend

Image formulae are available at links like http://rubyquiz.strd6.com/quizzes/191/1212289356.txt (replace the .png with .txt).


Saturday, February 07, 2009