CooCoo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/CooCoo.gemspec +47 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +88 -0
  6. data/README.md +123 -0
  7. data/Rakefile +81 -0
  8. data/bin/cuda-dev-info +25 -0
  9. data/bin/cuda-free +28 -0
  10. data/bin/cuda-free-trend +7 -0
  11. data/bin/ffi-gen +267 -0
  12. data/bin/spec_runner_html.sh +42 -0
  13. data/bin/trainer +198 -0
  14. data/bin/trend-cost +13 -0
  15. data/examples/char-rnn.rb +405 -0
  16. data/examples/cifar/cifar.rb +94 -0
  17. data/examples/img-similarity.rb +201 -0
  18. data/examples/math_ops.rb +57 -0
  19. data/examples/mnist.rb +365 -0
  20. data/examples/mnist_classifier.rb +293 -0
  21. data/examples/mnist_dream.rb +214 -0
  22. data/examples/seeds.rb +268 -0
  23. data/examples/seeds_dataset.txt +210 -0
  24. data/examples/t10k-images-idx3-ubyte +0 -0
  25. data/examples/t10k-labels-idx1-ubyte +0 -0
  26. data/examples/train-images-idx3-ubyte +0 -0
  27. data/examples/train-labels-idx1-ubyte +0 -0
  28. data/ext/buffer/Rakefile +50 -0
  29. data/ext/buffer/buffer.pre.cu +727 -0
  30. data/ext/buffer/matrix.pre.cu +49 -0
  31. data/lib/CooCoo.rb +1 -0
  32. data/lib/coo-coo.rb +18 -0
  33. data/lib/coo-coo/activation_functions.rb +344 -0
  34. data/lib/coo-coo/consts.rb +5 -0
  35. data/lib/coo-coo/convolution.rb +298 -0
  36. data/lib/coo-coo/core_ext.rb +75 -0
  37. data/lib/coo-coo/cost_functions.rb +91 -0
  38. data/lib/coo-coo/cuda.rb +116 -0
  39. data/lib/coo-coo/cuda/device_buffer.rb +240 -0
  40. data/lib/coo-coo/cuda/device_buffer/ffi.rb +109 -0
  41. data/lib/coo-coo/cuda/error.rb +51 -0
  42. data/lib/coo-coo/cuda/host_buffer.rb +117 -0
  43. data/lib/coo-coo/cuda/runtime.rb +157 -0
  44. data/lib/coo-coo/cuda/vector.rb +315 -0
  45. data/lib/coo-coo/data_sources.rb +2 -0
  46. data/lib/coo-coo/data_sources/xournal.rb +25 -0
  47. data/lib/coo-coo/data_sources/xournal/bitmap_stream.rb +197 -0
  48. data/lib/coo-coo/data_sources/xournal/document.rb +377 -0
  49. data/lib/coo-coo/data_sources/xournal/loader.rb +144 -0
  50. data/lib/coo-coo/data_sources/xournal/renderer.rb +101 -0
  51. data/lib/coo-coo/data_sources/xournal/saver.rb +99 -0
  52. data/lib/coo-coo/data_sources/xournal/training_document.rb +78 -0
  53. data/lib/coo-coo/data_sources/xournal/training_document/constants.rb +15 -0
  54. data/lib/coo-coo/data_sources/xournal/training_document/document_maker.rb +89 -0
  55. data/lib/coo-coo/data_sources/xournal/training_document/document_reader.rb +105 -0
  56. data/lib/coo-coo/data_sources/xournal/training_document/example.rb +37 -0
  57. data/lib/coo-coo/data_sources/xournal/training_document/sets.rb +76 -0
  58. data/lib/coo-coo/debug.rb +8 -0
  59. data/lib/coo-coo/dot.rb +129 -0
  60. data/lib/coo-coo/drawing.rb +4 -0
  61. data/lib/coo-coo/drawing/cairo_canvas.rb +100 -0
  62. data/lib/coo-coo/drawing/canvas.rb +68 -0
  63. data/lib/coo-coo/drawing/chunky_canvas.rb +101 -0
  64. data/lib/coo-coo/drawing/sixel.rb +214 -0
  65. data/lib/coo-coo/enum.rb +17 -0
  66. data/lib/coo-coo/from_name.rb +58 -0
  67. data/lib/coo-coo/fully_connected_layer.rb +205 -0
  68. data/lib/coo-coo/generation_script.rb +38 -0
  69. data/lib/coo-coo/grapher.rb +140 -0
  70. data/lib/coo-coo/image.rb +286 -0
  71. data/lib/coo-coo/layer.rb +67 -0
  72. data/lib/coo-coo/layer_factory.rb +26 -0
  73. data/lib/coo-coo/linear_layer.rb +59 -0
  74. data/lib/coo-coo/math.rb +607 -0
  75. data/lib/coo-coo/math/abstract_vector.rb +121 -0
  76. data/lib/coo-coo/math/functions.rb +39 -0
  77. data/lib/coo-coo/math/interpolation.rb +7 -0
  78. data/lib/coo-coo/network.rb +264 -0
  79. data/lib/coo-coo/neuron.rb +112 -0
  80. data/lib/coo-coo/neuron_layer.rb +168 -0
  81. data/lib/coo-coo/option_parser.rb +18 -0
  82. data/lib/coo-coo/platform.rb +17 -0
  83. data/lib/coo-coo/progress_bar.rb +11 -0
  84. data/lib/coo-coo/recurrence/backend.rb +99 -0
  85. data/lib/coo-coo/recurrence/frontend.rb +101 -0
  86. data/lib/coo-coo/sequence.rb +187 -0
  87. data/lib/coo-coo/shell.rb +2 -0
  88. data/lib/coo-coo/temporal_network.rb +291 -0
  89. data/lib/coo-coo/trainer.rb +21 -0
  90. data/lib/coo-coo/trainer/base.rb +67 -0
  91. data/lib/coo-coo/trainer/batch.rb +82 -0
  92. data/lib/coo-coo/trainer/batch_stats.rb +27 -0
  93. data/lib/coo-coo/trainer/momentum_stochastic.rb +59 -0
  94. data/lib/coo-coo/trainer/stochastic.rb +47 -0
  95. data/lib/coo-coo/transformer.rb +272 -0
  96. data/lib/coo-coo/vector_layer.rb +194 -0
  97. data/lib/coo-coo/version.rb +3 -0
  98. data/lib/coo-coo/weight_deltas.rb +23 -0
  99. data/prototypes/convolution.rb +116 -0
  100. data/prototypes/linear_drop.rb +51 -0
  101. data/prototypes/recurrent_layers.rb +79 -0
  102. data/www/images/screamer.png +0 -0
  103. data/www/images/screamer.xcf +0 -0
  104. data/www/index.html +82 -0
  105. metadata +373 -0
@@ -0,0 +1,205 @@
1
+ require 'coo-coo/consts'
2
+ require 'coo-coo/math'
3
+ require 'coo-coo/debug'
4
+ require 'coo-coo/layer_factory'
5
+ require 'coo-coo/weight_deltas'
6
+
7
+ module CooCoo
8
+ class FullyConnectedLayer
9
+ LayerFactory.register_type(self)
10
+
11
+ attr_reader :bias
12
+ attr_reader :weights
13
+ attr_reader :activation_function
14
+
15
+ def initialize(num_inputs, size, activation_func = ActivationFunctions::Identity.instance, weights = nil, bias = nil)
16
+ @num_inputs = num_inputs
17
+ @size = size
18
+ @activation_function = activation_func
19
+ @weights = weights || @activation_function.initial_weights(num_inputs, size)
20
+ @bias = bias || @activation_function.initial_bias(size)
21
+ end
22
+
23
+ def activation_function
24
+ ActivationFunctions::Identity.instance
25
+ end
26
+
27
+ def num_inputs
28
+ @num_inputs
29
+ end
30
+
31
+ def size
32
+ @size
33
+ end
34
+
35
+ def forward(input, hidden_state)
36
+ return @weights.dot(num_inputs, size, input, 1, num_inputs) + @bias, hidden_state
37
+ end
38
+
39
+ def backprop(input, output, errors, hidden_state)
40
+ return errors, hidden_state
41
+ end
42
+
43
+ def transfer_error(deltas)
44
+ deltas.dot(size, 1, @weights, num_inputs, size)
45
+ end
46
+
47
+ def transfer_input_error(expecting)
48
+ (output - expecting).to_a
49
+ end
50
+
51
+ def update_weights!(inputs, deltas)
52
+ adjust_weights!(weight_deltas(inputs, deltas))
53
+ end
54
+
55
+ def adjust_weights!(deltas)
56
+ @bias -= deltas.bias_deltas
57
+ @weights -= deltas.weight_deltas
58
+ self
59
+ end
60
+
61
+ def weight_deltas(inputs, deltas)
62
+ WeightDeltas.new(deltas, deltas.dot(1, size, inputs, num_inputs, 1))
63
+ end
64
+
65
+ def to_hash(network = nil)
66
+ { type: self.class.to_s,
67
+ outputs: size,
68
+ neurons: neuron_hash,
69
+ f: activation_function.name
70
+ }
71
+ end
72
+
73
+ def neuron_hash
74
+ @weights.each_slice(num_inputs).with_index.collect do |neuron_weights, i|
75
+ { num_inputs: num_inputs,
76
+ weights: neuron_weights.to_a,
77
+ bias: @bias[i]
78
+ }
79
+ end
80
+ end
81
+
82
+ def add_neurons!(new_size)
83
+ if new_size != @size
84
+ w = CooCoo::Vector.zeros(num_inputs * new_size)
85
+ w[0, @weights.size] = @weights
86
+ w[@weights.size, num_inputs] = @activation_function.initial_weights(num_inputs, 1)
87
+ @weights = w
88
+
89
+ @bias = CooCoo::Vector.ones(new_size).set(@bias)
90
+ @bias[-1] = @activation_function.initial_bias(1)[0]
91
+
92
+ @size = new_size
93
+ end
94
+
95
+ self
96
+ end
97
+
98
+ def add_inputs!(new_size)
99
+ if new_size != num_inputs
100
+ w = CooCoo::Vector.zeros(new_size * size)
101
+ w.set2d!(new_size, @weights, num_inputs, 0, 0)
102
+ w.set2d!(new_size, @activation_function.initial_weights(size, 1), 1, new_size - 1, 0)
103
+ @weights = w
104
+ @num_inputs = new_size
105
+ end
106
+
107
+ self
108
+ end
109
+
110
+ def update_neuron_from_hash!(neuron_index, h)
111
+ if neuron_index > size
112
+ add_neurons!(neuron_index)
113
+ end
114
+
115
+ @weights[neuron_index * num_inputs, num_inputs] = h[:weights]
116
+ @bias[neuron_index] = h[:bias]
117
+ end
118
+
119
+ def update_from_hash!(h)
120
+ add_neurons!(h[:outputs])
121
+ add_inputs!(h[:neurons][0][:num_inputs])
122
+
123
+ h[:outputs].times do |i|
124
+ update_neuron_from_hash!(i, h[:neurons][i])
125
+ end
126
+
127
+ self
128
+ end
129
+
130
+ def ==(other)
131
+ other.kind_of?(self.class) &&
132
+ size == other.size &&
133
+ bias == other.bias &&
134
+ weights == other.weights &&
135
+ activation_function == other.activation_function
136
+ end
137
+
138
+ class << self
139
+ def from_hash(h, network = nil)
140
+ self.new(h[:neurons][0][:num_inputs],
141
+ h[:outputs],
142
+ ActivationFunctions.from_name(h[:f] || 'Identity')).
143
+ update_from_hash!(h)
144
+ end
145
+ end
146
+ end
147
+ end
148
+
149
+ if __FILE__ == $0
150
+ require 'coo-coo/network'
151
+ require 'coo-coo/linear_layer'
152
+
153
+ activation = ENV.fetch('ACTIVATION', 'Logistic')
154
+ net = CooCoo::Network.new
155
+ fc_layer = CooCoo::FullyConnectedLayer.new(4, 2, CooCoo::ActivationFunctions.from_name('Identity'), CooCoo::Vector.ones(4 * 2), CooCoo::Vector.ones(2))
156
+ net.layer(fc_layer)
157
+ net.layer(CooCoo::LinearLayer.new(2, CooCoo::ActivationFunctions.from_name(activation)))
158
+
159
+ inputs = [ [ 1.0, 0.0, 0.0, 0.0 ],
160
+ [ 0.0, 0.0, 1.0, 0.0 ],
161
+ [ 0.0, 1.0, 0.0, 0.0],
162
+ [ 0.0, 0.0, 0.0, 1.0 ]
163
+ ].collect do |v|
164
+ CooCoo::CUDA::Vector[v]
165
+ end
166
+ targets = [ [ 1.0, 0.0 ],
167
+ [ 0.0, 1.0 ],
168
+ [ 0.0, 0.0 ],
169
+ [ 0.0, 0.0 ]
170
+ ].collect do |v|
171
+ CooCoo::CUDA::Vector[v]
172
+ end
173
+
174
+ inputs.zip(targets).cycle(ENV.fetch('LOOPS', 100).to_i).each do |(input, target)|
175
+ output, hidden_state = net.forward(input, Hash.new)
176
+ puts("#{input} -> #{target} #{target.inspect}")
177
+ puts("\toutput: #{output}")
178
+
179
+ err = (output.last - target)
180
+ puts("\terr: #{err}")
181
+ #err = err * err * 0.5
182
+ delta, hidden_state = net.backprop(input, output, err, hidden_state)
183
+ puts("\tdelta: #{delta}")
184
+ puts("\terror: #{err}")
185
+ puts("\txfer: #{net.transfer_errors(delta)}")
186
+
187
+ net.update_weights!(input, output, delta * 0.5)
188
+ end
189
+
190
+ new_net = CooCoo::Network.new()
191
+ h = fc_layer.to_hash
192
+ h[:type] = 'CooCoo::VectorLayer'
193
+ h[:neurons].each do |n|
194
+ n[:f] = activation
195
+ end
196
+ new_net.layer(CooCoo::Layer.from_hash(h))
197
+
198
+ puts("\nInput\tFully\tVector\tTarget")
199
+ inputs.zip(targets).each do |(input, target)|
200
+ oa, hsa = net.forward(input, Hash.new)
201
+ ob, hsb = new_net.forward(input, Hash.new)
202
+
203
+ puts("#{input} -> #{oa.last}\t#{ob.last}\t#{target}")
204
+ end
205
+ end
@@ -0,0 +1,38 @@
1
+ require 'coo-coo/option_parser'
2
+
3
+ module CooCoo
4
+ class GenerationScript
5
+ EvalBinding = Struct.new(:log)
6
+ EvalBinding.class_eval do
7
+ CooCoo = ::CooCoo
8
+
9
+ def get_binding
10
+ binding
11
+ end
12
+ end
13
+
14
+ attr_reader :opts
15
+
16
+ def initialize(path, log)
17
+ @path = path
18
+ @log = log
19
+ load(path)
20
+ end
21
+
22
+ def load(path)
23
+ env = EvalBinding.new(@log)
24
+ @generator, @opts = eval(File.read(path), env.get_binding, path)
25
+ @path = path
26
+ self
27
+ end
28
+
29
+ def parse_args(argv)
30
+ @opts.parse!(argv)
31
+ end
32
+
33
+ def call(argv, *args)
34
+ argv = parse_args(argv)
35
+ [ argv, @generator.call(*args) ]
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,140 @@
1
+ require 'coo-coo/dot'
2
+
3
+ module CooCoo
4
+ class Grapher
5
+ def initialize()
6
+ end
7
+
8
+ def populate(name, network, edge_widths = nil)
9
+ Dot::Graph.new(:digraph, :label => name, :ranksep => 3) do |g|
10
+ populate_inputs(network.num_inputs, g)
11
+ populate_layers(network.layers, edge_widths, g)
12
+ populate_outputs(network.num_outputs, network.num_layers - 1, edge_widths, g)
13
+ end
14
+ end
15
+
16
+ def populate_layers(layers, edge_widths, g)
17
+ layers.each_with_index do |l, i|
18
+ populate_layer(l, i, edge_widths && edge_widths[i], g)
19
+ end
20
+ end
21
+
22
+ def populate_inputs(num, g)
23
+ g.add_subgraph("cluster_inputs", :label => "Inputs", :rank => "same") do |sg|
24
+ inputs = num.times.collect do |i|
25
+ "input_#{i}"
26
+ end
27
+
28
+ sg.add_block("") do |ssg|
29
+ inputs.each_with_index do |name, i|
30
+ ssg.add_node(name, :label => i)
31
+ end
32
+ ssg.add_edge(inputs, :style => "invis")
33
+ end
34
+ end
35
+ end
36
+
37
+ def populate_layer(layer, layer_index, edge_widths, g)
38
+ g.add_subgraph("cluster_layer_#{layer_index}", :label => "Layer #{layer_index}") do |sg|
39
+ sg.add_subgraph("layer_#{layer_index}", :rank => "same") do |ssg|
40
+ nodes = layer.neurons.each_with_index.collect do |n, ni|
41
+ name = "neuron_#{layer_index}_#{ni}"
42
+ populate_neuron_node(name, ni, ssg)
43
+ name
44
+ end
45
+ ssg.add_edge(nodes, :style => "invis")
46
+ end
47
+
48
+ layer.neurons.each_with_index do |n, ni|
49
+ populate_neuron_link(n, ni, layer_index, edge_widths, sg)
50
+ end
51
+ end
52
+ end
53
+
54
+ def populate_neuron_node(neuron_id, neuron_index, g)
55
+ g.add_node(neuron_id, :label => neuron_index)
56
+ end
57
+
58
+ def populate_neuron_link(neuron, neuron_index, layer_index, edge_widths, g)
59
+ neuron.weights.each_with_index do |w, wi|
60
+ w = (edge_widths && edge_widths[wi]) || (w / 10.0)
61
+ #w = w / 10.0
62
+
63
+ if layer_index == 0
64
+ g.add_edge([ "input_#{wi}", "neuron_#{layer_index}_#{neuron_index}" ],
65
+ :penwidth => pen_scale(w.abs),
66
+ :color => pen_color(w))
67
+ else
68
+ g.add_edge([ "neuron_#{layer_index - 1}_#{wi}", "neuron_#{layer_index}_#{neuron_index}"],
69
+ :penwidth => pen_scale(w.abs),
70
+ :color => pen_color(w))
71
+ end
72
+ end
73
+ end
74
+
75
+ def pen_color(x)
76
+ x = pen_scale(x)
77
+ color = NMatrix[[ 0, 0, 0 ]]
78
+
79
+ if x > 0.01
80
+ color = NMatrix[[ 1, 0, 0 ]]
81
+ elsif x < 0.01
82
+ color = NMatrix[[ 0, 0, 1 ]]
83
+ end
84
+
85
+ '#' + color.to_a.collect { |n| (n.abs * 255).to_i.to_s(16).rjust(2, "0") }.join
86
+ end
87
+
88
+ def populate_outputs(num_outputs, last_layer, edge_widths, g)
89
+ g.add_subgraph("cluster_outputs", :label => "Outputs") do |sg|
90
+ num_outputs.times do |o|
91
+ sg.add_node("output_#{o}", :label => o)
92
+ end
93
+ end
94
+
95
+ num_outputs.times do |o|
96
+ w = edge_widths && edge_widths[-1][o] || 1.0
97
+ g.add_edge([ "neuron_#{last_layer}_#{o}", "output_#{o}" ],
98
+ :penwidth => pen_scale(w.abs),
99
+ :pencolor => pen_color(w))
100
+ end
101
+ end
102
+
103
+ def pen_scale(x)
104
+ x / 10.0
105
+ end
106
+ end
107
+
108
+ class OutputGrapher < Grapher
109
+ def populate(name, network, input_and_outputs)
110
+ super(name, network, input_and_outputs)
111
+ end
112
+
113
+ def pen_scale(x)
114
+ 2.0 * x
115
+ end
116
+ end
117
+
118
+ end
119
+
120
+ if __FILE__ == $0
121
+ require 'coo-coo'
122
+ net = CooCoo::Network.load(ARGV[0])
123
+ dw = CooCoo::Dot::Writer.new
124
+ ng = CooCoo::OutputGrapher.new
125
+ input = if ARGV[1]
126
+ ARGV[1].split.collect(&:to_f).to_nm([1, net.num_inputs])
127
+ else
128
+ m = NMatrix.zeros([1, net.num_inputs])
129
+ m[0] = 1.0
130
+ m
131
+ end
132
+ target = NMatrix.zeros([1, net.num_outputs])
133
+ target[0] = 1.0
134
+ target[-1] = 1.0
135
+ outputs = net.forward(input)
136
+ deltas = net.backprop(outputs, target)
137
+ err = net.transfer_errors(deltas)
138
+ graph = ng.populate(ARGV[0], net, [ input ] + outputs)
139
+ dw.write(graph, $stdout)
140
+ end
@@ -0,0 +1,286 @@
1
+ require 'nmatrix'
2
+
3
+ module CooCoo
4
+ module Image
5
+ class Base
6
+ attr_reader :width, :height, :bpp, :background
7
+ attr_accessor :repeat_x, :repeat_y
8
+
9
+ def initialize(width, height, bpp = 1, pixels = nil, background = nil, repeat_x = false, repeat_y = false)
10
+ @width = width
11
+ @height = height
12
+ @bpp = bpp
13
+ @span = @width * @bpp
14
+ @background = background || bpp.times.collect { 0 }
15
+ @repeat_x = repeat_x
16
+ @repeat_y = repeat_y
17
+ @pixels = pixels || Array.new(@width * @height * @bpp, 0)
18
+ end
19
+
20
+ def [](x, y, byte = nil)
21
+ if (@repeat_x == false && (x < 0 || x >= width)) ||
22
+ (@repeat_y == false && (y < 0 || y >= height))
23
+ p = @background
24
+ if byte
25
+ p[byte]
26
+ else
27
+ p
28
+ end
29
+ else
30
+ i = pixel_index(x, y, byte || 0)
31
+ if byte
32
+ @pixels[i] || @background[byte]
33
+ else
34
+ p = @pixels[i, @bpp]
35
+ if p && !p.empty?
36
+ p
37
+ else
38
+ @background
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ def []=(x, y, v)
45
+ @bpp.times do |byte|
46
+ c = v
47
+ if v.respond_to?(:[])
48
+ c = v[byte]
49
+ end
50
+ @pixels[*pixel_index(x, y, byte)] = c
51
+ end
52
+ end
53
+
54
+ def *(transform)
55
+ TransformedImage.new(self, transform)
56
+ end
57
+
58
+ def filter(f)
59
+ TransformedImage.new(self, nil, f)
60
+ end
61
+
62
+ def to_a
63
+ @pixels.each_slice(@span).collect do |row|
64
+ if @bpp > 1
65
+ row.each_slice(@bpp).to_a
66
+ else
67
+ row.to_a
68
+ end
69
+ end
70
+ end
71
+
72
+ private
73
+ def pixel_index(x, y, byte = 0)
74
+ (byte || 0) + ((x.round % @width) * @bpp) + ((y.round % @height) * @span)
75
+ end
76
+ end
77
+
78
+ class TransformedImage
79
+ def initialize(image, transform, filter = nil)
80
+ @image = image
81
+ @transform = transform
82
+ @filter = filter
83
+ end
84
+
85
+ def width
86
+ @image.width
87
+ end
88
+
89
+ def height
90
+ @image.height
91
+ end
92
+
93
+ def bpp
94
+ @image.bpp
95
+ end
96
+
97
+ def *(transform)
98
+ t = if @transform
99
+ TransformChain.new(@transform, transform)
100
+ else
101
+ transform
102
+ end
103
+ TransformedImage.new(@image, t, @filter)
104
+ end
105
+
106
+ def to_a
107
+ height.times.collect do |y|
108
+ width.times.collect do |x|
109
+ self[x, y]
110
+ end.flatten
111
+ end
112
+ end
113
+
114
+ def [](x, y, byte = nil)
115
+ x, y = *transform(x, y)
116
+ p = @image[x, y, byte]
117
+ filter(p, x, y)
118
+ end
119
+
120
+ def filter(pixel, x, y)
121
+ if @filter
122
+ @filter.call(pixel, x, y)
123
+ else
124
+ pixel
125
+ end
126
+ end
127
+
128
+ def transform(x, y)
129
+ if @transform
130
+ @transform.call(x, y)
131
+ else
132
+ [ x, y ]
133
+ end
134
+ end
135
+ end
136
+
137
+ class Transform
138
+ def call(x, y)
139
+ [ x, y ]
140
+ end
141
+
142
+ def *(other)
143
+ TransformChain.new(self, other)
144
+ end
145
+ end
146
+
147
+ class TransformChain < Transform
148
+ def initialize(first, second)
149
+ @first = first
150
+ @second = second
151
+ end
152
+
153
+ def call(x, y)
154
+ p = @second.call(x, y)
155
+ p2 = @first.call(*p)
156
+ #puts("#{self.inspect} #{x} #{y} -> #{p} -> #{p2}")
157
+ p2
158
+ end
159
+ end
160
+
161
+ class Clipper < Transform
162
+ def initialize(width, height)
163
+ @width = width
164
+ @height = height
165
+ end
166
+
167
+ def call(pixel, x, y)
168
+ if x < 0 || x >= @width || y < 0 || y >= @height
169
+ Array.new(pixel.size, 0.0)
170
+ else
171
+ pixel
172
+ end
173
+ end
174
+ end
175
+
176
+ class Translate < Transform
177
+ def initialize(tx, ty)
178
+ super()
179
+ @tx = tx
180
+ @ty = ty
181
+ end
182
+
183
+ def call(x, y)
184
+ [ x - @tx, y - @ty ]
185
+ end
186
+ end
187
+
188
+ class Scale < Transform
189
+ def initialize(sx, sy)
190
+ super()
191
+ @sx = sx
192
+ @sy = sy || sx
193
+ end
194
+
195
+ def call(x, y)
196
+ [ (x / @sx).floor, (y / @sy).floor ]
197
+ end
198
+ end
199
+
200
+ class Rotate < Transform
201
+ def initialize(ox, oy, radians)
202
+ super()
203
+ @ox = ox
204
+ @oy = oy
205
+ @radians = radians
206
+ end
207
+
208
+ def call(x, y)
209
+ c = ::Math.cos(@radians)
210
+ s = ::Math.sin(@radians)
211
+
212
+ x = x - @ox
213
+ y = y - @oy
214
+
215
+ nx = x * c - y * s
216
+ ny = x * s + y * c
217
+
218
+ nx = nx + @ox
219
+ ny = ny + @oy
220
+
221
+ [ nx.floor, ny.floor ]
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+
228
+ if __FILE__ == $0
229
+ def print_image(img)
230
+ img.height.times do |y|
231
+ puts(img.to_a[y].collect { |c| (c > 0.5)? 'X' : '.' }.join)
232
+ end
233
+ end
234
+
235
+ img = CooCoo::Image::Base.new(16, 16)
236
+ img.height.times do |i|
237
+ img[i, 0] = 1.0
238
+ img[0, i] = 1.0
239
+ end
240
+
241
+ puts("Image")
242
+ print_image(img)
243
+
244
+ t = CooCoo::Image::Translate.new(-3, -5)
245
+ puts("Translated")
246
+ print_image(img * t)
247
+
248
+ s = CooCoo::Image::Scale.new(2.0, 2.0)
249
+ puts("Scaled 2.0")
250
+ print_image(img * s)
251
+
252
+ s = CooCoo::Image::Scale.new(2.0, 2.0)
253
+ puts("Scaled 2.0 and transformed")
254
+ print_image(img * s * t)
255
+ puts("Translated and scaled 2.0")
256
+ print_image(img * t * s)
257
+
258
+ s = CooCoo::Image::Scale.new(0.5, 0.5)
259
+ puts("Scaled 0.5")
260
+ print_image(img * s)
261
+
262
+ r = CooCoo::Image::Rotate.new(img.width / 2.0, img.height / 2.0, ::Math::PI / 4.0)
263
+ puts("Rotated #{::Math::PI / 4.0} #{180.0 / 4.0}")
264
+ print_image(img * r)
265
+
266
+ r = CooCoo::Image::Rotate.new(0.0, 0.0, ::Math::PI / 3.0)
267
+ puts("Rotated #{::Math::PI / 3.0} #{180.0 / 3.0}")
268
+ print_image(img * r)
269
+
270
+ t = CooCoo::Image::Rotate.new(img.width / 2.0, img.height / 2.0, ::Math::PI / 3.0) * CooCoo::Image::Translate.new(3, 3)
271
+ puts("Rotated #{::Math::PI / 3.0} #{180.0 / 3.0}")
272
+ print_image(img * t)
273
+
274
+ c = CooCoo::Image::Clipper.new(8, 8)
275
+ puts("Clipped")
276
+ print_image(img.filter(c))
277
+ print_image(img.filter(c) * r)
278
+
279
+ RotationSteps = 7.0
280
+ (RotationSteps.to_i + 1).times do |i|
281
+ r = CooCoo::Image::Rotate.new(img.width / 2.0, img.height / 2.0, i * 2.0 * ::Math::PI / RotationSteps)
282
+ puts("#{i} Rotated #{i * 2.0 * ::Math::PI / RotationSteps} #{i * 360.0 / RotationSteps}")
283
+ print_image(img * r * s)
284
+ print_image(img * s * r)
285
+ end
286
+ end