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.
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:
- generate (or accept as input) mathematical formulae
- compute the RGB values for each pixel
- output an image
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:

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:






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