photomosaic 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/dtan4/photomosaic.svg?branch=master)](https://travis-ci.org/dtan4/photomosaic)
|
3
|
+
[![Coverage Status](https://coveralls.io/repos/dtan4/photomosaic/badge.png)](https://coveralls.io/r/dtan4/photomosaic)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/dtan4/photomosaic.png)](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
|