photomosaic 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/.rspec +2 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/Guardfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +32 -0
- data/Rakefile +7 -0
- data/bin/photomosaic +5 -0
- data/diagrams/sequence.png +0 -0
- data/diagrams/sequence.uml +70 -0
- data/diagrams/structure.png +0 -0
- data/diagrams/structure.uml +21 -0
- data/lib/photomosaic.rb +12 -0
- data/lib/photomosaic/client.rb +57 -0
- data/lib/photomosaic/color/hsv.rb +27 -0
- data/lib/photomosaic/color/rgb.rb +62 -0
- data/lib/photomosaic/image.rb +113 -0
- data/lib/photomosaic/image_downloader.rb +39 -0
- data/lib/photomosaic/options.rb +80 -0
- data/lib/photomosaic/search_engine/bing.rb +15 -0
- data/lib/photomosaic/version.rb +3 -0
- data/photomosaic.gemspec +31 -0
- data/spec/fixtures/lena.png +0 -0
- data/spec/fixtures/lena_0.png +0 -0
- data/spec/fixtures/lena_1.png +0 -0
- data/spec/fixtures/lena_2.png +0 -0
- data/spec/fixtures/lena_3.png +0 -0
- data/spec/fixtures/lena_4.png +0 -0
- data/spec/fixtures/lena_5.png +0 -0
- data/spec/photomosaic/client_spec.rb +91 -0
- data/spec/photomosaic/color/hsv_spec.rb +16 -0
- data/spec/photomosaic/color/rgb_spec.rb +52 -0
- data/spec/photomosaic/image_downloader_spec.rb +74 -0
- data/spec/photomosaic/image_spec.rb +151 -0
- data/spec/photomosaic/options_spec.rb +58 -0
- data/spec/photomosaic/search_engine/bing_spec.rb +57 -0
- data/spec/photomosaic_spec.rb +7 -0
- data/spec/spec_helper.rb +17 -0
- metadata +226 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7723e9d41a717de2d939a43a5f3e1adcf26ef177
|
4
|
+
data.tar.gz: b2dc7d9424dcf7da90c67c2b5adf712e702178a6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d85772d554437ff18364f922d4149c8602d69e94367d1274c8ef139164da361b716982dc0bc3a844140e783303447581ce8b798d0dde29f796df826f73fd1f4f
|
7
|
+
data.tar.gz: 0a72143a5f51cf00854fe5233dee7d42fe40e2534d6e63233d25e6c15a93bad66a954fa58e60eb8f22514aa43f4fdb7d30d23750f1ba2123fb5be60735741a9d
|
data/.gitignore
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
23
|
+
.envrc
|
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 dtan4
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# Photomosaic
|
2
|
+
[](https://travis-ci.org/dtan4/photomosaic)
|
3
|
+
[](https://coveralls.io/r/dtan4/photomosaic)
|
4
|
+
[](https://codeclimate.com/github/dtan4/photomosaic)
|
5
|
+
|
6
|
+
TODO: Write a gem description
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
gem 'photomosaic'
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
$ bundle
|
17
|
+
|
18
|
+
Or install it yourself as:
|
19
|
+
|
20
|
+
$ gem install photomosaic
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
TODO: Write usage instructions here
|
25
|
+
|
26
|
+
## Contributing
|
27
|
+
|
28
|
+
1. Fork it ( https://github.com/[my-github-username]/photomosaic/fork )
|
29
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
30
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
31
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
32
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/bin/photomosaic
ADDED
Binary file
|
@@ -0,0 +1,70 @@
|
|
1
|
+
@startuml
|
2
|
+
|
3
|
+
actor User
|
4
|
+
participant Client
|
5
|
+
participant Bing
|
6
|
+
participant ImageDownloader
|
7
|
+
participant BaseImage
|
8
|
+
participant PixelImage
|
9
|
+
database Web
|
10
|
+
|
11
|
+
User -> Client : Execute command
|
12
|
+
activate Client
|
13
|
+
create Bing
|
14
|
+
Client -> Bing : << new >>
|
15
|
+
Client -> Bing : Request to search images
|
16
|
+
activate Bing
|
17
|
+
Bing -> Web : Request to search images
|
18
|
+
activate Web
|
19
|
+
Web -> Web : Search images
|
20
|
+
Web -> Bing : List of image urls
|
21
|
+
deactivate Web
|
22
|
+
Bing -> Client : List of image urls
|
23
|
+
deactivate Bing
|
24
|
+
|
25
|
+
create ImageDownloader
|
26
|
+
Client -> ImageDownloader : << new >>
|
27
|
+
Client -> ImageDownloader : Request to download images
|
28
|
+
activate ImageDownloader
|
29
|
+
ImageDownloader -> Web : Request images
|
30
|
+
activate Web
|
31
|
+
Web -> ImageDownloader : Download images
|
32
|
+
deactivate Web
|
33
|
+
ImageDownloader -> ImageDownloader : Save downloaded images
|
34
|
+
ImageDownloader -> Client: List of downloaded image paths
|
35
|
+
deactivate ImageDownloader
|
36
|
+
|
37
|
+
create BaseImage
|
38
|
+
Client -> BaseImage : << new >>
|
39
|
+
BaseImage -> BaseImage : Preprocess
|
40
|
+
|
41
|
+
create PixelImage
|
42
|
+
Client -> PixelImage : << new >>
|
43
|
+
|
44
|
+
Client -> BaseImage : Request to dispatch pixel images
|
45
|
+
activate BaseImage
|
46
|
+
BaseImage -> PixelImage : Request characteristic color
|
47
|
+
activate PixelImage
|
48
|
+
PixelImage -> PixelImage : Calculate characteristic color
|
49
|
+
PixelImage -> BaseImage : Characteristic color
|
50
|
+
deactivate PixelImage
|
51
|
+
BaseImage -> BaseImage : Pick up the "nearest" image of the pixel
|
52
|
+
BaseImage -> Client : Map of dispatched images
|
53
|
+
deactivate BaseImage
|
54
|
+
|
55
|
+
Client -> PixelImage : Request to resize to pixel size
|
56
|
+
activate PixelImage
|
57
|
+
PixelImage -> PixelImage : Resize to pixel size
|
58
|
+
PixelImage -> Client : Done
|
59
|
+
deactivate PixelImage
|
60
|
+
|
61
|
+
Client->PixelImage : Request to compose mosaic image
|
62
|
+
activate PixelImage
|
63
|
+
PixelImage->PixelImage : Compose mosaic image
|
64
|
+
PixelImage->Client : Done
|
65
|
+
deactivate PixelImage
|
66
|
+
|
67
|
+
Client->User : Done
|
68
|
+
deactivate Client
|
69
|
+
|
70
|
+
@enduml
|
Binary file
|
@@ -0,0 +1,21 @@
|
|
1
|
+
@startuml
|
2
|
+
|
3
|
+
package Photomosaic {
|
4
|
+
class Client
|
5
|
+
class Image
|
6
|
+
class ImageDownloader
|
7
|
+
class Options
|
8
|
+
|
9
|
+
Client o-- Options
|
10
|
+
|
11
|
+
package SearchEngine {
|
12
|
+
class Bing
|
13
|
+
}
|
14
|
+
|
15
|
+
package Color {
|
16
|
+
class HSV
|
17
|
+
class RGB
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
21
|
+
@enduml
|
data/lib/photomosaic.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require "photomosaic/client"
|
2
|
+
require "photomosaic/color/hsv"
|
3
|
+
require "photomosaic/color/rgb"
|
4
|
+
require "photomosaic/image"
|
5
|
+
require "photomosaic/image_downloader"
|
6
|
+
require "photomosaic/options"
|
7
|
+
require "photomosaic/search_engine/bing"
|
8
|
+
require "photomosaic/version"
|
9
|
+
|
10
|
+
module Photomosaic
|
11
|
+
# Your code goes here...
|
12
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Photomosaic
|
2
|
+
class Client
|
3
|
+
def initialize(argv)
|
4
|
+
@options = Photomosaic::Options.parse(argv)
|
5
|
+
end
|
6
|
+
|
7
|
+
def execute
|
8
|
+
@image_downloader = Photomosaic::ImageDownloader.new
|
9
|
+
|
10
|
+
begin
|
11
|
+
resized_images = Photomosaic::Image.resize_images(pixel_images, 40, 20)
|
12
|
+
Photomosaic::Image.create_mosaic_image(resized_images, @options.output_path)
|
13
|
+
ensure
|
14
|
+
@image_downloader.remove_save_dir
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def base_image
|
21
|
+
@base_image ||= Photomosaic::Image.preprocess_image(
|
22
|
+
@options.base_image,
|
23
|
+
@options.width,
|
24
|
+
@options.height,
|
25
|
+
@options.level,
|
26
|
+
@options.colors
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def image_path_list
|
31
|
+
@image_path_list ||= @image_downloader.download_images(image_url_list)
|
32
|
+
end
|
33
|
+
|
34
|
+
def image_url_list
|
35
|
+
@image_url_list ||= search_engine.get_image_list(@options.keyword)
|
36
|
+
end
|
37
|
+
|
38
|
+
def image_list
|
39
|
+
@image_list ||= image_path_list.map do |path|
|
40
|
+
begin
|
41
|
+
Photomosaic::Image.new(path)
|
42
|
+
rescue Magick::ImageMagickError
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
end.compact
|
46
|
+
end
|
47
|
+
|
48
|
+
def pixel_images
|
49
|
+
@pixel_images ||=
|
50
|
+
base_image.dispatch_images(image_list, 1, 2, @options.color_model)
|
51
|
+
end
|
52
|
+
|
53
|
+
def search_engine
|
54
|
+
@options.search_engine.new(@options.api_key, @options.results)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Photomosaic
|
2
|
+
module Color
|
3
|
+
class HSV
|
4
|
+
attr_reader :hue, :saturation, :value
|
5
|
+
|
6
|
+
def initialize(hue, saturation, value)
|
7
|
+
@hue = hue
|
8
|
+
@saturation = saturation
|
9
|
+
@value = value
|
10
|
+
end
|
11
|
+
|
12
|
+
def calculate_distance(hsv)
|
13
|
+
Math.sqrt(squares_array(hsv).inject(&:+))
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def squares_array(hsv)
|
19
|
+
[
|
20
|
+
(self.hue - hsv.hue)**2,
|
21
|
+
(self.saturation - hsv.saturation)**2,
|
22
|
+
(self.value - hsv.value)**2
|
23
|
+
]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Photomosaic
|
2
|
+
module Color
|
3
|
+
class RGB
|
4
|
+
attr_reader :red, :green, :blue
|
5
|
+
|
6
|
+
def initialize(red, green, blue)
|
7
|
+
@red = red
|
8
|
+
@green = green
|
9
|
+
@blue = blue
|
10
|
+
end
|
11
|
+
|
12
|
+
def max
|
13
|
+
@max ||= [@red, @green, @blue].max
|
14
|
+
end
|
15
|
+
|
16
|
+
def min
|
17
|
+
@min ||= [@red, @green, @blue].min
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_hsv
|
21
|
+
HSV.new(hue, saturation, value)
|
22
|
+
end
|
23
|
+
|
24
|
+
def calculate_distance(rgb)
|
25
|
+
Math.sqrt(squares_array(rgb).inject(&:+))
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def hue
|
31
|
+
return 0 if max == min
|
32
|
+
|
33
|
+
_hue = case max
|
34
|
+
when @red
|
35
|
+
((@green - @blue).to_f / (max - min)) % 6
|
36
|
+
when @green
|
37
|
+
(@blue - @red).to_f / (max - min) + 2
|
38
|
+
else
|
39
|
+
(@red - @green).to_f / (max - min) + 4
|
40
|
+
end
|
41
|
+
|
42
|
+
(_hue * 60).to_i
|
43
|
+
end
|
44
|
+
|
45
|
+
def saturation
|
46
|
+
max == 0 ? 0 : (max - min).to_f / max * 100
|
47
|
+
end
|
48
|
+
|
49
|
+
def squares_array(rgb)
|
50
|
+
[
|
51
|
+
(self.red - rgb.red)**2,
|
52
|
+
(self.green - rgb.green)**2,
|
53
|
+
(self.blue - rgb.blue)**2
|
54
|
+
]
|
55
|
+
end
|
56
|
+
|
57
|
+
def value
|
58
|
+
(max * 100).to_f / 256
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require "RMagick"
|
2
|
+
|
3
|
+
module Photomosaic
|
4
|
+
class Image
|
5
|
+
def self.create_mosaic_image(image_list, output_path)
|
6
|
+
image_list.inject(Magick::ImageList.new) do |images, row|
|
7
|
+
images << row.inject(Magick::ImageList.new) do |col_images, image|
|
8
|
+
col_images << image.image.dup
|
9
|
+
col_images
|
10
|
+
end.append(false)
|
11
|
+
|
12
|
+
images
|
13
|
+
end.append(true).write(output_path)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.preprocess_image(image_path, width, height, level, colors)
|
17
|
+
image = Photomosaic::Image.new(image_path)
|
18
|
+
image.resize!(width, height, true)
|
19
|
+
image.posterize!(level)
|
20
|
+
image.reduce_colors!(colors)
|
21
|
+
image
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.resize_images(images, width, height)
|
25
|
+
images.map do |row|
|
26
|
+
row.map { |image| image.resize!(width, height, false) }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(image_path)
|
31
|
+
@image = Magick::Image.read(image_path).first
|
32
|
+
end
|
33
|
+
|
34
|
+
def characteristic_color(color_model = :rgb)
|
35
|
+
@characteristic_color ||= get_characteristic_color(color_model)
|
36
|
+
end
|
37
|
+
|
38
|
+
def dispatch_images(source_image_list, row_step = 1, col_step = 1, color_model = :rgb)
|
39
|
+
(1..image_height).step(row_step).inject([]) do |images, y|
|
40
|
+
images << (1..image_width).step(col_step).inject([]) do |col_images, x|
|
41
|
+
col_images << nearest_image(source_image_list, x, y, color_model)
|
42
|
+
col_images
|
43
|
+
end
|
44
|
+
|
45
|
+
images
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def image
|
50
|
+
@image
|
51
|
+
end
|
52
|
+
|
53
|
+
def posterize!(level)
|
54
|
+
@image = @image.posterize(level)
|
55
|
+
reload_image
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
def reduce_colors!(colors)
|
60
|
+
@image = @image.quantize(colors)
|
61
|
+
reload_image
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
def resize!(width, height, keep_ratio)
|
66
|
+
keep_ratio ? @image.resize_to_fit!(width, height) : @image.resize!(width, height)
|
67
|
+
reload_image
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def get_characteristic_color(color_model = :rgb)
|
74
|
+
color = nil
|
75
|
+
original_image = @image.dup
|
76
|
+
|
77
|
+
begin
|
78
|
+
resize!(1, 1, false)
|
79
|
+
color = pixel_color(1, 1, color_model)
|
80
|
+
ensure
|
81
|
+
@image = original_image
|
82
|
+
end
|
83
|
+
|
84
|
+
color
|
85
|
+
end
|
86
|
+
|
87
|
+
def nearest_image(source_image_list, x, y, color_model)
|
88
|
+
pixel = pixel_color(x, y, color_model)
|
89
|
+
|
90
|
+
source_image_list.sort_by do |image|
|
91
|
+
image.characteristic_color(color_model).calculate_distance(pixel)
|
92
|
+
end.first
|
93
|
+
end
|
94
|
+
|
95
|
+
def pixel_color(x, y, color_model = :rgb)
|
96
|
+
pixel = @image.pixel_color(x, y)
|
97
|
+
rgb = Color::RGB.new(pixel.red / 257, pixel.green / 257, pixel.blue / 257)
|
98
|
+
color_model == :rgb ? rgb : rgb.to_hsv
|
99
|
+
end
|
100
|
+
|
101
|
+
def reload_image
|
102
|
+
@image = Magick::Image.from_blob(@image.to_blob).first
|
103
|
+
end
|
104
|
+
|
105
|
+
def image_height
|
106
|
+
@image.rows
|
107
|
+
end
|
108
|
+
|
109
|
+
def image_width
|
110
|
+
@image.columns
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|