CooCoo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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