Raster graphics operations/Ruby: Difference between revisions

mNo edit summary
 
(3 intermediate revisions by 3 users not shown)
Line 1:
<span style='font-family: "Linux Libertine",Georgia,Times,serif;font-size:150%;'>[[Ruby]]</span><hr>
{{implementation|Raster graphics operations}}[[Category:Ruby]]
==The Code==
Collecting all the Ruby code from [[:Category:Raster graphics operations]], so one can invoke: <code>require 'raster_graphics'</code>
 
Uses the [https://github.com/wvanbergen/chunky_png ChunkyPNG] pure-Ruby PNG library. Important to note won't work with
`frozen_string_literal: true` uses mutable String operations.
 
<lang ruby>###########################################################################
# frozen_string_literal: false
# Represents an RGB[http://en.wikipedia.org/wiki/Rgb] colour.
 
###########################################################################
# 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) }
raise ArgumentError, "invalid RGB parameters: #{[red, green, blue].inspect}" unless ok
unless ok
 
raise ArgumentError, "invalid RGB parameters: #{[red, green, blue].inspect}"
end@red = red
@red, @green, @blue = red, green, blue
@blue = blue
end
attr_reader :red, :green, :blue
alias_methodalias :r, :red
alias_methodalias :g, :green
alias_methodalias :b, :blue
 
# Return the list of [red, green, blue] values.
Line 41 ⟶ 49:
#
def <=>(a_colour)
self.luminosity <=> a_colour.luminosity
end
 
Line 50 ⟶ 58:
#
def luminosity
Integer(0.2126 * @red + 0.7152 * @green + 0.0722 * @blue)
end
 
Line 67 ⟶ 75:
# 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
 
Line 92 ⟶ 100:
 
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
Line 102 ⟶ 110:
 
###############################################
def [](x, y)
validate_pixel(x, y)
@data[x][y]
end
alias_methodalias :get_pixel, :[]
 
def []=(x, y, colour)
validate_pixel(x, y)
@data[x][y] = colour
end
alias_methodalias :set_pixel, :[]=
 
def each_pixel
if block_given?
@height.times { |y| @width.times { |x| yield x, y } }
else
to_enum(:each_pixel)
Line 124 ⟶ 132:
###############################################
# write to file/stream
PIXMAP_FORMATS = %w["P3", "P6"] .freeze # implemented output formats
PIXMAP_BINARY_FORMATS = ["'P6"'] .freeze # implemented output formats which are binary
 
def write_ppm(ios, format =" 'P6"')
ifraise NotImplementedError, "pixmap format #{format} has not been implemented" unless PIXMAP_FORMATS.include?(format)
 
raise NotImplementedError, "pixmap format #{format} has not been implemented"
ios.puts format, "#{@width} #{@height}", "'255"'
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:format=>" 'P6"' })
File.open(filename, 'w') do |f|
write_ppm(f, opts[:format])
end
end
alias_methodalias :write, :save
 
def print(opts = { format:format=>" 'P6"' })
write_ppm($stdout, opts[:format])
end
 
def save_as_jpeg(filename, quality = 75)
# using the ImageMagick convert tool
 
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
begin
pipe.close rescue false
pipe = IO.popen("convert ppm:- -quality #{quality} jpg:#{filename}", 'w')
rescue StandardError
write_ppm(pipe)
endfalse
rescue SystemCallError => e
warn "problem writing data to 'convert' utility -- does it exist in your $PATH?"
ensure
pipe.close rescue false
end
end
Line 166 ⟶ 176:
def save_as_png(filename)
require 'chunky_png'
stream = StringIO.new('', 'r+')
#require 'stringio'
each_pixel { |x, y| stream =<< StringIO.new(""self[x, "r+"y].values.pack('ccc') }
each_pixel {|x, y| stream << self[x, y].values.pack("ccc")}
stream.seek(0)
ChunkyPNG::Canvas.extend(ChunkyPNG::Canvas::StreamImporting)
Line 179 ⟶ 188:
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"
Line 191 ⟶ 199:
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
Line 209 ⟶ 217:
 
def self.open_from_jpeg(filename)
raise ArgumentError, "#{filename} does not exists or is not readable." unless File.readable?(filename)
 
raise ArgumentError, "#{filename} does not exists or is not readable."
end
begin
pipe = IO.popen("convert jpg:#{filename} ppm:-", 'r')
Line 218 ⟶ 225:
warn "problem reading data from 'convert' utility -- does it exist in your $PATH?"
ensure
pipe.close rescue falsebegin
pipe.close
rescue StandardError
false
end
end
end
Line 226 ⟶ 237:
def to_grayscale
gray = self.class.new(@width, @height)
each_pixel do |x, y|
gray[x, y] = self[x, y].to_grayscale
end
gray
Line 248 ⟶ 259:
# 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
Line 263 ⟶ 274:
validate_pixel(p2.x, p2.y)
 
x1, y1 = p1.x, p1.y
x2, y2y1 = p2.x, p2p1.y
x2 = p2.x
y2 = p2.y
 
steep = (y2 - y1).abs > (x2 - x1).abs
if steep
Line 280 ⟶ 293:
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.negative?
y += ystep
error += deltax
Line 295 ⟶ 308:
###############################################
def draw_line_antialised(p1, p2, colour)
x1, y1 = p1.x, p1.y
x2, y2y1 = p2.x, p2p1.y
x2 = p2.x
y2 = p2.y
 
steep = (y2 - y1).abs > (x2 - x1).abs
if steep
Line 310 ⟶ 325:
deltay = (y2 - y1).abs
gradient = 1.0 * deltay / deltax
 
# handle the first endpoint
xend = x1.round
Line 320 ⟶ 335:
put_colour(xpxl1, ypxl1 + 1, colour, steep, yend.fpart * xgap)
itery = yend + gradient
 
# handle the second endpoint
xend = x2.round
Line 329 ⟶ 344:
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
Line 344 ⟶ 359:
 
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
Line 351 ⟶ 366:
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
Line 389 ⟶ 404:
until queue.empty?
p = queue.dequeue
ifnext unless self[p.x, p.y] == current_colour
 
west = find_border(p, current_colour, :west)
eastwest = find_border(p, current_colour, :eastwest)
east = draw_linefind_border(westp, eastcurrent_colour, new_colour:east)
draw_line(west, east, q = westnew_colour)
while q.x <= east.xwest
while q.x <= east.x
[:north, :south].each do |direction|
%i[north south].each do n = neighbour(q, |direction)|
n = queue.enqueueneighbour(n) if self[n.xq, n.y] == current_colourdirection)
queue.enqueue(n) if self[n.x, n.y] == current_colour
end
q = neighbour(q, :east)
end
q = neighbour(q, :east)
end
end
Line 424 ⟶ 439:
 
###############################################
def median_filter(radius = 3)
radius += 1 if radius.even?
radius += 1
end
filtered = self.class.new(@width, @height)
 
 
$stdout.puts "processing #{@height} rows"
Line 439 ⟶ 451:
(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.negative?
win_y = 0 if win_y < 0.negative?
win_x = @width - 1 if win_x >= @width
win_y = @height - 1 if win_y >= @height
window << self[win_x, win_y]
end
Line 460 ⟶ 472:
def magnify(factor)
bigger = self.class.new(@width * factor, @height * factor)
each_pixel do |x, y|
colour = self[x, y]
(x *factor factor.. x * factor + factor - 1).each do |xx|
(y *factor factor.. y * factor + factor - 1).each do |yy|
bigger[xx, yy] = colour
end
end
Line 474 ⟶ 486:
def histogram
histogram = Hash.new(0)
each_pixel do |x, y|
histogram[self[x, y].luminosity] += 1
end
histogram
end
 
Line 483 ⟶ 495:
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]
Line 500 ⟶ 512:
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
Line 520 ⟶ 532:
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
 
###############################################
Line 547 ⟶ 559:
# 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.negative?
0
elsif value > 255
Line 576 ⟶ 588:
end
end
 
 
###########################################################################
Line 586 ⟶ 597:
@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
Line 602 ⟶ 613:
end
 
class Queue # < Array
alias_methodalias :enqueue, :push
alias_methodalias :dequeue, :shift
end
 
class Numeric
def fpart
self - self.truncate
end
 
def rfpart
1.0 - self.fpart
end
end
Line 618 ⟶ 630:
class Integer
def choose(k)
self.factorial / (k.factorial * (self - k).factorial)
end
 
def factorial
(2 .. self).reduce(1, :*)
end
end</lang>
Anonymous user