⚠️ Warning: This is a draft ⚠️
This means it might contain formatting issues, incorrect code, conceptual problems, or other severe issues.
If you want to help to improve and eventually enable this page, please fork RosettaGit's repository and open a merge request on GitHub.
[[Category:Ruby]]
==The Code==
Collecting all the Ruby code from [[:Category:Raster graphics operations]], so one can invoke: require 'raster_graphics'
Uses the [https://github.com/wvanbergen/chunky_png ChunkyPNG] pure-Ruby PNG library.
###########################################################################
# Represents an RGB[http://en.wikipedia.org/wiki/Rgb] colour.
class RGBColour
# Red, green and blue values must fall in the range 0..255.
def initialize(red, green, blue)
ok = [red, green, blue].inject(true) {|ok,c| ok &= c.between?(0,255)}
unless ok
raise ArgumentError, "invalid RGB parameters: #{[red, green, blue].inspect}"
end
@red, @green, @blue = red, green, blue
end
attr_reader :red, :green, :blue
alias_method :r, :red
alias_method :g, :green
alias_method :b, :blue
# Return the list of [red, green, blue] values.
# RGBColour.new(100,150,200).values # => [100, 150, 200]
# call-seq:
# values -> array
#
def values
[@red, @green, @blue]
end
# Equality test: two RGBColour objects are equal if they have the same
# red, green and blue values.
# call-seq:
# ==(a_colour) -> true or false
#
def ==(a_colour)
values == a_colour.values
end
# Comparison test: compares two RGBColour objects based on their #luminosity value
# call-seq:
# <=>(a_colour) -> -1, 0, +1
#
def <=>(a_colour)
self.luminosity <=> a_colour.luminosity
end
# Calculate a integer luminosity value, in the range 0..255
# RGBColour.new(100,150,200).luminosity # => 142
# call-seq:
# luminosity -> int
#
def luminosity
Integer(0.2126*@red + 0.7152*@green + 0.0722*@blue)
end
# Return a new RGBColour value where all the red, green, blue values are the
# #luminosity value.
# RGBColour.new(100,150,200).to_grayscale.values # => [142, 142, 142]
# call-seq:
# to_grayscale -> a_colour
#
def to_grayscale
l = luminosity
self.class.new(l, l, l)
end
# Return a new RGBColour object given an iteration value for the Pixmap.mandelbrot
# method.
def self.mandel_colour(i)
self.new( 16*(i % 15), 32*(i % 7), 8*(i % 31) )
end
RED = RGBColour.new(255,0,0)
GREEN = RGBColour.new(0,255,0)
BLUE = RGBColour.new(0,0,255)
YELLOW= RGBColour.new(255,255,0)
BLACK = RGBColour.new(0,0,0)
WHITE = RGBColour.new(255,255,255)
end
###########################################################################
# A Pixel represents an (x,y) point in a Pixmap.
Pixel = Struct.new(:x, :y)
###########################################################################
class Pixmap
def initialize(width, height)
@width = width
@height = height
@data = fill(RGBColour::WHITE)
end
attr_reader :width, :height
def fill(colour)
@data = Array.new(@width) {Array.new(@height, colour)}
end
def validate_pixel(x,y)
unless x.between?(0, @width-1) and y.between?(0, @height-1)
raise ArgumentError, "requested pixel (#{x}, #{y}) is outside dimensions of this bitmap"
end
end
###############################################
def [](x,y)
validate_pixel(x,y)
@data[x][y]
end
alias_method :get_pixel, :[]
def []=(x,y,colour)
validate_pixel(x,y)
@data[x][y] = colour
end
alias_method :set_pixel, :[]=
def each_pixel
if block_given?
@height.times {|y| @width.times {|x| yield x,y}}
else
to_enum(:each_pixel)
end
end
###############################################
# write to file/stream
PIXMAP_FORMATS = ["P3", "P6"] # implemented output formats
PIXMAP_BINARY_FORMATS = ["P6"] # implemented output formats which are binary
def write_ppm(ios, format="P6")
if not PIXMAP_FORMATS.include?(format)
raise NotImplementedError, "pixmap format #{format} has not been implemented"
end
ios.puts format, "#{@width} #{@height}", "255"
ios.binmode if PIXMAP_BINARY_FORMATS.include?(format)
each_pixel do |x, y|
case format
when "P3" then ios.print @data[x][y].values.join(" "),"\n"
when "P6" then ios.print @data[x][y].values.pack('C3')
end
end
end
def save(filename, opts={:format=>"P6"})
File.open(filename, 'w') do |f|
write_ppm(f, opts[:format])
end
end
alias_method :write, :save
def print(opts={:format=>"P6"})
write_ppm($stdout, opts[:format])
end
def save_as_jpeg(filename, quality=75)
# using the ImageMagick convert tool
begin
pipe = IO.popen("convert ppm:- -quality #{quality} jpg:#{filename}", 'w')
write_ppm(pipe)
rescue SystemCallError => e
warn "problem writing data to 'convert' utility -- does it exist in your $PATH?"
ensure
pipe.close rescue false
end
end
def save_as_png(filename)
require 'chunky_png'
stream = StringIO.new("", "r+")
each_pixel {|x, y| stream << self[x, y].values.pack("ccc")}
stream.seek(0)
ChunkyPNG::Canvas.extend(ChunkyPNG::Canvas::StreamImporting)
canvas = ChunkyPNG::Canvas.from_rgb_stream(width, height, stream)
canvas.to_image.save(filename)
end
###############################################
# read from file/pipe
def self.read_ppm(ios)
format = ios.gets.chomp
width, height = ios.gets.chomp.split.map {|n| n.to_i }
max_colour = ios.gets.chomp
if (not PIXMAP_FORMATS.include?(format)) or
width < 1 or height < 1 or
max_colour != '255'
then
ios.close
raise StandardError, "file '#{filename}' does not start with the expected header"
end
ios.binmode if PIXMAP_BINARY_FORMATS.include?(format)
bitmap = self.new(width, height)
bitmap.each_pixel do |x,y|
# read 3 bytes
red, green, blue = case format
when 'P3' then ios.gets.chomp.split
when 'P6' then ios.read(3).unpack('C3')
end
bitmap[x,y] = RGBColour.new(red, green, blue)
end
ios.close
bitmap
end
def self.open(filename)
read_ppm(File.open(filename, 'r'))
end
def self.open_from_jpeg(filename)
unless File.readable?(filename)
raise ArgumentError, "#{filename} does not exists or is not readable."
end
begin
pipe = IO.popen("convert jpg:#{filename} ppm:-", 'r')
read_ppm(pipe)
rescue SystemCallError => e
warn "problem reading data from 'convert' utility -- does it exist in your $PATH?"
ensure
pipe.close rescue false
end
end
###############################################
# conversion methods
def to_grayscale
gray = self.class.new(@width, @height)
each_pixel do |x,y|
gray[x,y] = self[x,y].to_grayscale
end
gray
end
def to_blackandwhite
hist = histogram
# find the median luminosity
median = nil
sum = 0
hist.keys.sort.each do |lum|
sum += hist[lum]
if sum > @height * @width / 2
median = lum
break
end
end
# create the black and white image
bw = self.class.new(@width, @height)
each_pixel do |x,y|
bw[x,y] = self[x,y].luminosity < median ? RGBColour::BLACK : RGBColour::WHITE
end
bw
end
def save_as_blackandwhite(filename)
to_blackandwhite.save(filename)
end
###############################################
def draw_line(p1, p2, colour)
validate_pixel(p1.x, p2.y)
validate_pixel(p2.x, p2.y)
x1, y1 = p1.x, p1.y
x2, y2 = p2.x, p2.y
steep = (y2 - y1).abs > (x2 - x1).abs
if steep
x1, y1 = y1, x1
x2, y2 = y2, x2
end
if x1 > x2
x1, x2 = x2, x1
y1, y2 = y2, y1
end
deltax = x2 - x1
deltay = (y2 - y1).abs
error = deltax / 2
ystep = y1 < y2 ? 1 : -1
y = y1
x1.upto(x2) do |x|
pixel = steep ? [y,x] : [x,y]
self[*pixel] = colour
error -= deltay
if error < 0
y += ystep
error += deltax
end
end
end
###############################################
def draw_line_antialised(p1, p2, colour)
x1, y1 = p1.x, p1.y
x2, y2 = p2.x, p2.y
steep = (y2 - y1).abs > (x2 - x1).abs
if steep
x1, y1 = y1, x1
x2, y2 = y2, x2
end
if x1 > x2
x1, x2 = x2, x1
y1, y2 = y2, y1
end
deltax = x2 - x1
deltay = (y2 - y1).abs
gradient = 1.0 * deltay / deltax
# handle the first endpoint
xend = x1.round
yend = y1 + gradient * (xend - x1)
xgap = (x1 + 0.5).rfpart
xpxl1 = xend
ypxl1 = yend.truncate
put_colour(xpxl1, ypxl1, colour, steep, yend.rfpart * xgap)
put_colour(xpxl1, ypxl1 + 1, colour, steep, yend.fpart * xgap)
itery = yend + gradient
# handle the second endpoint
xend = x2.round
yend = y2 + gradient * (xend - x2)
xgap = (x2 + 0.5).rfpart
xpxl2 = xend
ypxl2 = yend.truncate
put_colour(xpxl2, ypxl2, colour, steep, yend.rfpart * xgap)
put_colour(xpxl2, ypxl2 + 1, colour, steep, yend.fpart * xgap)
# in between
(xpxl1 + 1).upto(xpxl2 - 1).each do |x|
put_colour(x, itery.truncate, colour, steep, itery.rfpart)
put_colour(x, itery.truncate + 1, colour, steep, itery.fpart)
itery = itery + gradient
end
end
def put_colour(x, y, colour, steep, c)
x, y = y, x if steep
self[x, y] = anti_alias(colour, self[x, y], c)
end
def anti_alias(new, old, ratio)
blended = new.values.zip(old.values).map {|n, o| (n*ratio + o*(1.0 - ratio)).round}
RGBColour.new(*blended)
end
###############################################
def draw_circle(pixel, radius, colour)
validate_pixel(pixel.x, pixel.y)
self[pixel.x, pixel.y + radius] = colour
self[pixel.x, pixel.y - radius] = colour
self[pixel.x + radius, pixel.y] = colour
self[pixel.x - radius, pixel.y] = colour
f = 1 - radius
ddF_x = 1
ddF_y = -2 * radius
x = 0
y = radius
while x < y
if f >= 0
y -= 1
ddF_y += 2
f += ddF_y
end
x += 1
ddF_x += 2
f += ddF_x
self[pixel.x + x, pixel.y + y] = colour
self[pixel.x + x, pixel.y - y] = colour
self[pixel.x - x, pixel.y + y] = colour
self[pixel.x - x, pixel.y - y] = colour
self[pixel.x + y, pixel.y + x] = colour
self[pixel.x + y, pixel.y - x] = colour
self[pixel.x - y, pixel.y + x] = colour
self[pixel.x - y, pixel.y - x] = colour
end
end
###############################################
def flood_fill(pixel, new_colour)
current_colour = self[pixel.x, pixel.y]
queue = Queue.new
queue.enqueue(pixel)
until queue.empty?
p = queue.dequeue
if self[p.x, p.y] == current_colour
west = find_border(p, current_colour, :west)
east = find_border(p, current_colour, :east)
draw_line(west, east, new_colour)
q = west
while q.x <= east.x
[:north, :south].each do |direction|
n = neighbour(q, direction)
queue.enqueue(n) if self[n.x, n.y] == current_colour
end
q = neighbour(q, :east)
end
end
end
end
def neighbour(pixel, direction)
case direction
when :north then Pixel[pixel.x, pixel.y - 1]
when :south then Pixel[pixel.x, pixel.y + 1]
when :east then Pixel[pixel.x + 1, pixel.y]
when :west then Pixel[pixel.x - 1, pixel.y]
end
end
def find_border(pixel, colour, direction)
nextp = neighbour(pixel, direction)
while self[nextp.x, nextp.y] == colour
pixel = nextp
nextp = neighbour(pixel, direction)
end
pixel
end
###############################################
def median_filter(radius=3)
if radius.even?
radius += 1
end
filtered = self.class.new(@width, @height)
$stdout.puts "processing #{@height} rows"
pb = ProgressBar.new(@height) if $DEBUG
@height.times do |y|
@width.times do |x|
window = []
(x - radius).upto(x + radius).each do |win_x|
(y - radius).upto(y + radius).each do |win_y|
win_x = 0 if win_x < 0
win_y = 0 if win_y < 0
win_x = @width-1 if win_x >= @width
win_y = @height-1 if win_y >= @height
window << self[win_x, win_y]
end
end
# median
filtered[x, y] = window.sort[window.length / 2]
end
pb.update(y) if $DEBUG
end
pb.close if $DEBUG
filtered
end
###############################################
def magnify(factor)
bigger = self.class.new(@width * factor, @height * factor)
each_pixel do |x,y|
colour = self[x,y]
(x*factor .. x*factor + factor-1).each do |xx|
(y*factor .. y*factor + factor-1).each do |yy|
bigger[xx,yy] = colour
end
end
end
bigger
end
###############################################
def histogram
histogram = Hash.new(0)
each_pixel do |x,y|
histogram[self[x,y].luminosity] += 1
end
histogram
end
###############################################
def draw_bezier_curve(points, colour)
# ensure the points are increasing along the x-axis
points = points.sort_by {|p| [p.x, p.y]}
xmin = points[0].x
xmax = points[-1].x
increment = 2
prev = points[0]
((xmin + increment) .. xmax).step(increment) do |x|
t = 1.0 * (x - xmin) / (xmax - xmin)
p = Pixel[x, bezier(t, points).round]
draw_line(prev, p, colour)
prev = p
end
end
# the generalized n-degree Bezier summation
def bezier(t, points)
n = points.length - 1
points.each_with_index.inject(0.0) do |sum, (point, i)|
sum += n.choose(i) * (1-t)**(n - i) * t**i * point.y
end
end
###############################################
def self.mandelbrot(width, height)
mandel = Pixmap.new(width,height)
pb = ProgressBar.new(width) if $DEBUG
width.times do |x|
height.times do |y|
x_ish = Float(x - width*11/15) / (width/3)
y_ish = Float(y - height/2) / (height*3/10)
mandel[x,y] = RGBColour.mandel_colour(mandel_iters(x_ish, y_ish))
end
pb.update(x) if $DEBUG
end
pb.close if $DEBUG
mandel
end
def self.mandel_iters(cx,cy)
x = y = 0.0
count = 0
while Math.hypot(x,y) < 2 and count < 255
x, y = (x**2 - y**2 + cx), (2*x*y + cy)
count += 1
end
count
end
###############################################
# Apply a convolution kernel to a whole image
def convolute(kernel)
newimg = Pixmap.new(@width, @height)
pb = ProgressBar.new(@width) if $DEBUG
@width.times do |x|
@height.times do |y|
apply_kernel(x, y, kernel, newimg)
end
pb.update(x) if $DEBUG
end
pb.close if $DEBUG
newimg
end
# Applies a convolution kernel to produce a single pixel in the destination
def apply_kernel(x, y, kernel, newimg)
x0 = [0, x-1].max
y0 = [0, y-1].max
x1 = x
y1 = y
x2 = [@width-1, x+1].min
y2 = [@height-1, y+1].min
r = g = b = 0.0
[x0, x1, x2].zip(kernel).each do |xx, kcol|
[y0, y1, y2].zip(kcol).each do |yy, k|
r += k * self[xx,yy].r
g += k * self[xx,yy].g
b += k * self[xx,yy].b
end
end
newimg[x,y] = RGBColour.new(luma(r), luma(g), luma(b))
end
# Function for clamping values to those that we can use with colors
def luma(value)
if value < 0
0
elsif value > 255
255
else
value
end
end
end
###########################################################################
# Utilities
class ProgressBar
def initialize(max)
$stdout.sync = true
@progress_max = max
@progress_pos = 0
@progress_view = 68
$stdout.print "[#{'-'*@progress_view}]\r["
end
def update(n)
new_pos = n * @progress_view/@progress_max
if new_pos > @progress_pos
@progress_pos = new_pos
$stdout.print '='
end
end
def close
$stdout.puts '=]'
end
end
class Queue < Array
alias_method :enqueue, :push
alias_method :dequeue, :shift
end
class Numeric
def fpart
self - self.truncate
end
def rfpart
1.0 - self.fpart
end
end
class Integer
def choose(k)
self.factorial / (k.factorial * (self - k).factorial)
end
def factorial
(2 .. self).reduce(1, :*)
end
end
==A Test Suite==
def display_pixmap(filename)
puts "displaying #{filename}"
system "./ppmview.rb #{filename} &"
end
###########################################################################
if $0 == __FILE__
old_debug = $DEBUG
$DEBUG = true
# for testing
class Pixmap
def ==(a_bitmap)
return false if @width != a_bitmap.width or @height != a_bitmap.height
@width.times {|x| @height.times {|y|
return false if not self[x,y] == (a_bitmap[x,y])
}}
true
end
end
require 'test/unit'
class TestRGBColour < Test::Unit::TestCase
def test_init
color = RGBColour.new(0,100,200)
assert_equal(100, color.g)
end
def test_constants
assert_equal([255,0,0], [RGBColour::RED.r,RGBColour::RED.g,RGBColour::RED.b])
assert_equal([0,255,0], [RGBColour::GREEN.r,RGBColour::GREEN.g,RGBColour::GREEN.b])
assert_equal([0,0,255], [RGBColour::BLUE.r,RGBColour::BLUE.g,RGBColour::BLUE.b])
end
def test_error
color = RGBColour.new(0,100,200)
assert_raise(ArgumentError) {RGBColour.new(0,0,256)}
end
end
class TestPixmap < Test::Unit::TestCase
def setup
@w = 20
@h = 30
@bitmap = Pixmap.new(@w,@h)
end
def test_init
assert_equal(@w, @bitmap.width)
assert_equal(@h, @bitmap.height)
assert_equal(RGBColour::WHITE, @bitmap.get_pixel(10,10))
end
def test_fill
@bitmap.fill(RGBColour::RED)
assert_equal(255,@bitmap[10,10].red)
assert_equal(0,@bitmap[10,10].green)
assert_equal(0,@bitmap[10,10].blue)
end
def test_get_pixel
assert_equal(@bitmap[5,6], @bitmap.get_pixel(5,6))
assert_raise(ArgumentError) {@bitmap[100,100]}
end
def test_grayscale
@bitmap.fill(RGBColour::BLUE)
@bitmap.height.times {|y| [9,10,11].each {|x| @bitmap[x,y]=RGBColour::GREEN}}
@bitmap.width.times {|x| [14,15,16].each {|y| @bitmap[x,y]=RGBColour::GREEN}}
@bitmap.save('testcross.ppm')
Pixmap.open('testcross.ppm').to_grayscale.save('testgray.ppm')
end
def test_save
@bitmap.fill(RGBColour::BLUE)
filename = 'test.ppm'
@bitmap.save(filename)
expected_size = 3 + (@w.to_s.length + 1 + @h.to_s.length + 1) + 4 + (@w * @h * 3)
assert_equal(expected_size, File.size(filename))
end
def test_open
@bitmap.fill(RGBColour::RED)
@bitmap.set_pixel(10,15, RGBColour::WHITE)
filename = 'test.ppm'
@bitmap.save(filename)
new = Pixmap.open(filename)
assert(@bitmap == new)
end
end
# a green cross on a blue background
colour_bitmap = Pixmap.new(20, 30)
colour_bitmap.fill(RGBColour::BLUE)
colour_bitmap.height.times {|y| [9,10,11].each {|x| colour_bitmap[x,y]=RGBColour::GREEN}}
colour_bitmap.width.times {|x| [14,15,16].each {|y| colour_bitmap[x,y]=RGBColour::GREEN}}
colour_bitmap.save('testcross.ppm')
display_pixmap 'testcross.ppm'
Pixmap.open('testcross.ppm').to_grayscale.save('testgray.ppm')
image = Pixmap.open('testcross.ppm')
image.save_as_jpeg('testcross.jpg')
#image.print(:format => "P3")
bitmap = Pixmap.open_from_jpeg('testcross.jpg')
savefile = 'testcross_from_jpeg.ppm'
bitmap.save(savefile)
display_pixmap savefile
bitmap = Pixmap.new(500, 500)
bitmap.fill(RGBColour::BLUE)
10.step(430, 60) do |a|
bitmap.draw_line(Pixel[10, 10], Pixel[490,a], RGBColour::YELLOW)
bitmap.draw_line(Pixel[10, 10], Pixel[a,490], RGBColour::YELLOW)
end
bitmap.draw_line(Pixel[10, 10], Pixel[490,490], RGBColour::YELLOW)
savefile = 'testlines4.ppm'
bitmap.save(savefile)
display_pixmap savefile
bitmap = Pixmap.new(30, 30)
bitmap.draw_circle(Pixel[14,14], 12, RGBColour::BLACK)
savefile = 'testcircle.ppm'
bitmap.save(savefile)
display_pixmap savefile
bitmap = Pixmap.new(300, 300)
bitmap.draw_circle(Pixel[149,149], 120, RGBColour::BLACK)
bitmap.draw_circle(Pixel[200,100], 40, RGBColour::BLACK)
bitmap.flood_fill(Pixel[140,160], RGBColour::BLUE)
savefile = 'testflood.ppm'
bitmap.save(savefile)
display_pixmap savefile
bitmap = Pixmap.new(500, 500)
bitmap.fill(RGBColour::BLUE)
10.step(430, 60) do |a|
bitmap.draw_line_antialised(Pixel[10, 10], Pixel[490,a], RGBColour::YELLOW)
bitmap.draw_line_antialised(Pixel[10, 10], Pixel[a,490], RGBColour::YELLOW)
end
bitmap.draw_line_antialised(Pixel[10, 10], Pixel[490,490], RGBColour::YELLOW)
bitmap.save('testantialias.ppm')
display_pixmap 'testantialias.ppm'
file = 'teapot.ppm'
display_pixmap file
bitmap = Pixmap.open(file)
# test new grayscale
savefile = 'teapotgray.ppm'
gray = bitmap.to_grayscale
gray.save(savefile)
display_pixmap savefile
#
savefile = 'testfiltered.ppm'
filtered = bitmap.median_filter
filtered.save(savefile)
display_pixmap savefile
file = 'teapot.ppm'
savefile = 'teapotbw.ppm'
display_pixmap file
Pixmap.open(file).save_as_blackandwhite(savefile)
display_pixmap savefile
bitmap = Pixmap.new(400, 400)
points = [
Pixel[40,100], Pixel[100,350], Pixel[150,50],
Pixel[150,150], Pixel[350,250], Pixel[250,250]
]
points.each {|p| bitmap.draw_circle(p, 3, RGBColour::RED)}
bitmap.draw_bezier_curve(points, RGBColour::BLUE)
savefile = 'testbezier.ppm'
bitmap.save(savefile)
display_pixmap savefile
savefile = 'testmandel.ppm'
Pixmap.mandelbrot(500,500).save(savefile)
display_pixmap savefile
# Demonstration code using the teapot image from Tk's widget demo
teapot = Pixmap.open('teapot.ppm')
[ ['Emboss', [[-2.0, -1.0, 0.0], [-1.0, 1.0, 1.0], [0.0, 1.0, 2.0]]],
['Sharpen', [[-1.0, -1.0, -1.0], [-1.0, 9.0, -1.0], [-1.0, -1.0, -1.0]]],
['Blur', [[0.1111,0.1111,0.1111],[0.1111,0.1111,0.1111],[0.1111,0.1111,0.1111]]],
].each do |label, kernel|
savefile = 'test' + label.downcase + '.ppm'
teapot.convolute(kernel).save(savefile)
display_pixmap savefile
end
$DEBUG = old_debug
end
==An Image Viewer== {{libheader|Ruby/Tk}}
The ppmview.rb
program is:
#!/usr/bin/ruby
require 'tk'
if ARGV.empty?
$stderr.puts "usage: #{File.basename($0)} imagefile"
exit 1
end
filename = ARGV.shift
unless File.readable?(filename)
raise ArgumentError, "can't read file '#{filename}'"
end
root = TkRoot.new('title' => File.basename(filename))
label = TkLabel.new(root) {image TkPhotoImage.new('file' => filename)}
label.pack
Tk.mainloop