Daniel Moore wrote:

> ## Mathematical Image Generator (#191)
> 
> 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.

...


> 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!

I think for a pure speed competition, we'd need a standard set of 
functions, a reference image, a defined way to translate -1..1 values to 
colour intensities (eg 0..255).

But it seemed like a fun one, so below is my offering: a little desktop 
(wxRuby) program for generating these maths images interactively.

Type the r, g and b functions into the boxes, then click "Render" to 
update. "Save Image" saves the current image to TIF, PNG or BMP.

If there's an error in a function, a cross mark appears next to it; 
hover over the cross mark to get a hint about the problem.

On my system, using Ruby 1.9, it renders the default image at 800 x 600 
in about 2.85s. Since the speed is pretty much proportional to the total 
pixels, I'd guess about 11.5s for a 1600x1200 image. Interestingly, this 
is one where Ruby 1.9 makes a big difference.

a

__________

require 'wx'
include Wx
include Math

# A canvas that draws and displays a mathematically generated image
class MathsDrawing < Window
   # The functions which return the colour components at each pixel
   attr_writer :red, :green, :blue
   # The time taken to render, whether re-rendering is needed, and the
   # source image
   attr_reader :render_time, :done, :img

   def initialize(parent)
     super(parent)
     # Create a dummy image
     @default_image = Image.new(1, 1)
     @default_image.data = [255, 255, 255].pack('CCC')
     @img = @default_image

     @red   = lambda { | x, y | 1 }
     @green = lambda { | x, y | 1 }
     @blue  = lambda { | x, y | 1 }

     @done = true

     evt_size :on_size
     evt_paint :on_paint
     evt_idle :on_idle
   end

   # Paint the image on the screen. The actual image rendering is done in
   # idle time, so that the GUI is responsive whilst redrawing - eg, when
   # resized. Painting is done by quickly rescaling the cached image.
   def on_paint
     paint do | dc |
       draw_img = @img.scale(client_size.x, client_size.y)
       dc.draw_bitmap(draw_img.convert_to_bitmap, 0, 0, true)
     end
   end

   # Regenerate the image if needed, then do a refresh
   def on_idle
     if not @done
       @img = make_image
       refresh
     end
     @done = true
   end

   # Note to regenerate the image if the canvas has been resized
   def on_size(event)
     @done = false
     event.skip
   end

   # Call this to force a re-render - eg if the functions have changed
   def redraw
     @done = false
   end

   # Actually make the image
   def make_image
     size_x, size_y = client_size.x, client_size.y
     if size_x < 1 or size_y < 1
       return @default_image
     end

     start_time = Time.now
     # 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.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
     img = Image.new(size_x, size_y)
     img.data = data
     @render_time = Time.now - start_time
     img
   end
end

# A helper dialog for saving the image to a file
class SaveImageDialog < FileDialog
   # The image file formats on offer
   TYPES = [ [ "PNG file (*.png)|*.png", BITMAP_TYPE_PNG ],
             [ "TIF file (*.tif)|*.tif", BITMAP_TYPE_TIF ],
             [ "BMP file (*.bmp)|*.bmp", BITMAP_TYPE_BMP ] ]

   WILDCARD = TYPES.map { | type | type.first }.join("|")

   def initialize(parent)
     super(parent, :wildcard => WILDCARD,
                   :message => 'Save Image',
                   :style => FD_SAVE|FD_OVERWRITE_PROMPT)
   end

   # Returns the Wx identifier for the selected image type.
   def image_type
     TYPES[filter_index].last
   end
end

# A Panel for displaying the image and controls to manipulate it
class MathsPanel < Panel
   # Set functions to some nice initial values
   RED_INITIAL   = "cos(x)"
   GREEN_INITIAL = "cos(y ** x)"
   BLUE_INITIAL  = "(x ** 4) + ( y ** 3 ) - (4.5 * x ** 2 ) + ( y * 2)"

   # Symbols to show correct and incorrect functions
   TICK  = "\xE2\x9C\x94"
   CROSS = "\xE2\x9C\x98"

   attr_reader :drawing

   def initialize(parent)
     super(parent)
     self.sizer = VBoxSizer.new
     # The canvas
     @drawing = MathsDrawing.new(self)
     sizer.add @drawing, 1, GROW

     sizer.add Wx::StaticLine.new(self)

     # The text controls for entering functions
     grid_sz = FlexGridSizer.new(3, 8, 8)
     grid_sz.add_growable_col(1, 1)

     grid_sz.add StaticText.new(self, :label => "Red")
     @red_tx = TextCtrl.new(self, :value => RED_INITIAL)
     grid_sz.add @red_tx, 0, GROW
     @red_err = StaticText.new(self, :label => TICK)
     grid_sz.add @red_err, 0, ALIGN_CENTRE

     grid_sz.add StaticText.new(self, :label => "Green")
     @green_tx = TextCtrl.new(self, :value => GREEN_INITIAL)
     grid_sz.add @green_tx, 0, GROW
     @green_err = StaticText.new(self, :label => TICK)
     grid_sz.add @green_err, 0, ALIGN_CENTRE

     grid_sz.add StaticText.new(self, :label => "Blue")
     @blue_tx = TextCtrl.new(self, :value => BLUE_INITIAL)
     grid_sz.add @blue_tx, 0, GROW
     @blue_err = StaticText.new(self, :label => TICK)
     grid_sz.add @blue_err, 0, ALIGN_CENTRE

     # Buttons to save and render
     grid_sz.add nil
     butt_sz = HBoxSizer.new
     render_bt = Button.new(self, :label => "Render")
     butt_sz.add render_bt, 0, Wx::RIGHT, 8
     evt_button render_bt, :on_render

     save_bt = Button.new(self, :label => "Save Image")
     butt_sz.add save_bt, 0, Wx::RIGHT, 8
     evt_button save_bt, :on_save

     # Disable the buttons whilst redrawing
     evt_update_ui(render_bt) { | evt | evt.enable(@drawing.done) }
     evt_update_ui(save_bt) { | evt | evt.enable(@drawing.done) }
     grid_sz.add butt_sz

     # Add the controls sizer to the whole thing
     sizer.add grid_sz, 0, GROW|ALL, 10

     on_render
   end

   # Update the functions that generate the image, then re-render it
   def on_render
     @drawing.red   = make_a_function(@red_tx.value, @red_err)
     @drawing.green = make_a_function(@green_tx.value, @green_err)
     @drawing.blue  = make_a_function(@blue_tx.value, @blue_err)
     @drawing.redraw
   end

   # Display a dialog to save the image to a file
   def on_save
     dlg = SaveImageDialog.new(parent)
     if dlg.show_modal == ID_OK
       @drawing.img.save_file(dlg.path, dlg.image_type)
     end
   end

   # A function which doesn't do anything
   NULL_FUNC = lambda { | x, y | 1 }

   # Takes a string source +source+, returns a lambda. If the string
   # source isn't valid, flag this in the GUI static text +error_outlet+
   def make_a_function(source, error_outlet)
     return NULL_FUNC if source.empty?
     func = nil
     begin
       # Create the function and test it, to check for wrong names
       func = eval "lambda { | x, y | #{source} }"
       func.call(0, 0)
     rescue Exception => e
       error_outlet.label = CROSS
       error_outlet.tool_tip = e.class.name + ":\n" +
                               e.message.sub(/^\(eval\):\d+: /, '')
       return NULL_FUNC
     end

     error_outlet.label = TICK
     error_outlet.tool_tip = ''
     func
   end
end

class MathsFrame < Frame
   def initialize
     super(nil, :title => 'Maths drawing',
                :size => [400, 500],
                :pos => [50, 50])
     sb = create_status_bar(1)
     evt_update_ui sb, :on_update_status
     @panel = MathsPanel.new(self)
   end

   def on_update_status
     if @panel.drawing.done
       pixels = @panel.drawing.client_size
       msg = "[#{pixels.x} x #{pixels.y}] drawing completed in " +
             "#{@panel.drawing.render_time}s"
       status_bar.status_text = msg
     end
   end
end

App.run do
   MathsFrame.new.show
end