photomosaic 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,39 @@
1
+ require "fileutils"
2
+ require "open-uri"
3
+ require "tmpdir"
4
+
5
+ module Photomosaic
6
+ class ImageDownloader
7
+ def initialize(save_dir = tmp_dir)
8
+ @save_dir = save_dir
9
+ end
10
+
11
+ def download_images(image_url_list)
12
+ image_url_list.inject([]) do |path_list, image_url|
13
+ image_path = File.join(@save_dir, File.basename(image_url))
14
+
15
+ begin
16
+ download_image(image_url, image_path)
17
+ path_list << image_path
18
+ rescue
19
+ end
20
+
21
+ path_list
22
+ end
23
+ end
24
+
25
+ def remove_save_dir
26
+ FileUtils.remove_entry_secure(@save_dir)
27
+ end
28
+
29
+ private
30
+
31
+ def download_image(image_url, image_path)
32
+ open(image_path, "wb+") { |f| f.puts open(image_url).read }
33
+ end
34
+
35
+ def tmp_dir
36
+ Dir.mktmpdir("photomosaic")
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,80 @@
1
+ require "optparse"
2
+
3
+ module Photomosaic
4
+ class Options
5
+ KEYS = %w(
6
+ api_key
7
+ base_image
8
+ color_model
9
+ colors
10
+ height
11
+ keyword
12
+ level
13
+ output_path
14
+ results
15
+ search_engine
16
+ width
17
+ )
18
+
19
+ REQUIRED_KEYS = %w(
20
+ api_key
21
+ base_image
22
+ output_path
23
+ )
24
+
25
+ def self.parse(argv)
26
+ options = default_options
27
+ parser(options).parse(argv)
28
+ options[:api_key] = api_key
29
+ check_options(options)
30
+
31
+ self.new(options)
32
+ end
33
+
34
+ def initialize(options)
35
+ @options = options
36
+ end
37
+
38
+ KEYS.each do |key|
39
+ define_method(key) { @options[key.to_sym] }
40
+ end
41
+
42
+ private
43
+
44
+ def self.api_key
45
+ ENV["PHOTOMOSAIC_API_KEY"]
46
+ end
47
+
48
+ def self.check_options(options)
49
+ REQUIRED_KEYS.each do |key|
50
+ raise OptionParser::MissingArgument, key unless options[key.to_sym]
51
+ end
52
+ end
53
+
54
+ def self.default_options
55
+ {
56
+ color_model: :rgb,
57
+ colors: 16,
58
+ height: 200,
59
+ level: 4,
60
+ results: 50,
61
+ search_engine: Photomosaic::SearchEngine::Bing,
62
+ width: 200,
63
+ }
64
+ end
65
+
66
+ def self.parser(options)
67
+ OptionParser.new do |opt|
68
+ opt.on("-b", "--base_image=VAL", "Path of base image") { |val| options[:base_image] = File.expand_path(val) }
69
+ opt.on("-c", "--colors=VAL", "Number of colors") { |val| options[:colors] = val.to_i }
70
+ opt.on("-h", "--height=VAL", "Height of output image") { |val| options[:height] = val.to_i }
71
+ opt.on("-k", "--keyword=VAL", "Keyword") { |val| options[:keyword] = val }
72
+ opt.on("-l", "--level=VAL", "Color level") { |val| options[:level] = val.to_i }
73
+ opt.on("-o", "--output_path=VAL", "Path of mosaic image") { |val| options[:output_path] = File.expand_path(val) }
74
+ opt.on("-r", "--results=VAL", "Number of results") { |val| options[:results] = val.to_i }
75
+ opt.on("-w", "--width=VAL", "Width of output image") { |val| options[:width] = val.to_i }
76
+ opt.on("--hsv", "Use HSV") { |val| options[:color_model] = :hsv }
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,15 @@
1
+ require "searchbing"
2
+
3
+ module Photomosaic
4
+ module SearchEngine
5
+ class Bing
6
+ def initialize(api_key, results)
7
+ @client = ::Bing.new(api_key, results, "Image")
8
+ end
9
+
10
+ def get_image_list(keyword)
11
+ @client.search(keyword)[0][:Image].map { |image| image[:MediaUrl] }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module Photomosaic
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'photomosaic/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "photomosaic"
8
+ spec.version = Photomosaic::VERSION
9
+ spec.authors = ["Daisuke Fujita"]
10
+ spec.email = ["dtanshi45@gmail.com"]
11
+ spec.summary = %q{Photomosaic Generator}
12
+ spec.description = %q{Photomosaic Generator}
13
+ spec.homepage = "https://github.com/dtan4/photomosaic"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "searchbing"
22
+ spec.add_dependency "rmagick"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.5"
25
+ spec.add_development_dependency "coveralls"
26
+ spec.add_development_dependency "guard-rspec"
27
+ spec.add_development_dependency "rake"
28
+ spec.add_development_dependency "rspec", "~> 3.0.0"
29
+ spec.add_development_dependency "terminal-notifier-guard"
30
+ spec.add_development_dependency "webmock"
31
+ end
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,91 @@
1
+ require "spec_helper"
2
+ require "ostruct"
3
+
4
+ module Photomosaic
5
+ describe Client do
6
+ let(:api_key) { "api_key" }
7
+ let(:base_image) { fixture_path("lena.png") }
8
+ let(:color_model) { :rgb }
9
+ let(:colors) { 16 }
10
+ let(:height) { 200 }
11
+ let(:keyword) { "keyword" }
12
+ let(:level) { 4 }
13
+ let(:output_path) { tmp_path("output.png") }
14
+ let(:results) { 50 }
15
+ let(:search_engine) { SearchEngine::Bing }
16
+ let(:width) { 200 }
17
+
18
+ let(:options) do
19
+ {
20
+ api_key: api_key,
21
+ base_image: base_image,
22
+ color_model: color_model,
23
+ colors: colors,
24
+ height: height,
25
+ keyword: keyword,
26
+ level: level,
27
+ output_path: output_path,
28
+ results: results,
29
+ search_engine: search_engine,
30
+ width: width
31
+ }
32
+ end
33
+
34
+ let(:client) do
35
+ described_class.new("argv")
36
+ end
37
+
38
+ let(:dispatched_images) do
39
+ [
40
+ [image],
41
+ [image],
42
+ [image]
43
+ ]
44
+ end
45
+
46
+ let(:image) do
47
+ double(Photomosaic::Image)
48
+ end
49
+
50
+ let(:image_name_list) do
51
+ [
52
+ "lena_0.png",
53
+ "lena_1.png",
54
+ "lena_2.png",
55
+ "notfound.png"
56
+ ]
57
+ end
58
+
59
+ let(:image_path_list) do
60
+ image_name_list.map { |name| fixture_path(name) }
61
+ end
62
+
63
+ let(:image_url_list) do
64
+ image_name_list.map { |name| "http://example.com/#{name}" }
65
+ end
66
+
67
+ before do
68
+ allow(Photomosaic::Options).to receive(:parse).and_return(OpenStruct.new(options))
69
+ end
70
+
71
+ describe "#execute" do
72
+ before do
73
+ allow_any_instance_of(Photomosaic::SearchEngine::Bing).to receive(:get_image_list).and_return(image_url_list)
74
+ allow_any_instance_of(Photomosaic::ImageDownloader).to receive(:download_images).and_return(image_path_list)
75
+ allow(Photomosaic::Image).to receive(:create_mosaic_image)
76
+ allow(Photomosaic::Image).to receive(:new).with(/lena(?:_\d)?\.png/).and_return(image)
77
+ allow(Photomosaic::Image).to receive(:new).with(/notfound.png/).and_raise Magick::ImageMagickError
78
+ allow(Photomosaic::Image).to receive(:resize_to_pixel_size)
79
+ allow(image).to receive(:dispatch_images).and_return(dispatched_images)
80
+ allow(image).to receive(:posterize!)
81
+ allow(image).to receive(:reduce_colors!)
82
+ allow(image).to receive(:resize!)
83
+ end
84
+
85
+ it "should execute the program" do
86
+ expect(Photomosaic::Image).to receive(:create_mosaic_image)
87
+ client.execute
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,16 @@
1
+ require "spec_helper"
2
+
3
+ module Photomosaic
4
+ module Color
5
+ describe HSV do
6
+ describe "#calculate_distance" do
7
+ it "should calculate color distance" do
8
+ color_a = described_class.new(100, 50.0, 70.0)
9
+ color_b = described_class.new(50, 30.0, 50.0)
10
+
11
+ expect(color_a.calculate_distance(color_b)).to be_within(0.1).of(57.4)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,52 @@
1
+ require "spec_helper"
2
+
3
+ module Photomosaic
4
+ module Color
5
+ describe RGB do
6
+ describe "#calculate_distance" do
7
+ it "should calculate color distance" do
8
+ color_a = described_class.new(50, 100, 200)
9
+ color_b = described_class.new(10, 20, 30)
10
+
11
+ expect(color_a.calculate_distance(color_b)).to be_within(0.5).of(192.0)
12
+ end
13
+ end
14
+
15
+ describe "#to_hsv" do
16
+ # cf. http://tech-unlimited.com/color.html
17
+
18
+ it "should convert (50, 50, 50) to (0, 0, 20)" do
19
+ rgb = described_class.new(50, 50, 50)
20
+ hsv = rgb.to_hsv
21
+ expect(hsv.hue).to eq 0
22
+ expect(hsv.saturation).to be_within(1).of(0)
23
+ expect(hsv.value).to be_within(1).of(20)
24
+ end
25
+
26
+ it "should convert (50, 100, 200) to (220, 75, 78)" do
27
+ rgb = described_class.new(50, 100, 200)
28
+ hsv = rgb.to_hsv
29
+ expect(hsv.hue).to eq 220
30
+ expect(hsv.saturation).to be_within(1).of(75)
31
+ expect(hsv.value).to be_within(1).of(78)
32
+ end
33
+
34
+ it "should convert (100, 50, 200) to (260, 75, 78)" do
35
+ rgb = described_class.new(100, 50, 200)
36
+ hsv = rgb.to_hsv
37
+ expect(hsv.hue).to eq 260
38
+ expect(hsv.saturation).to be_within(1).of(75)
39
+ expect(hsv.value).to be_within(1).of(78)
40
+ end
41
+
42
+ it "should convert (100, 200, 50) to (100, 75, 78)" do
43
+ rgb = described_class.new(100, 200, 50)
44
+ hsv = rgb.to_hsv
45
+ expect(hsv.hue).to eq 100
46
+ expect(hsv.saturation).to be_within(1).of(75)
47
+ expect(hsv.value).to be_within(1).of(78)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,74 @@
1
+ require "spec_helper"
2
+ require "fileutils"
3
+ require "webmock/rspec"
4
+
5
+ module Photomosaic
6
+ describe ImageDownloader do
7
+ let(:downloader) do
8
+ described_class.new(tmp_dir)
9
+ end
10
+
11
+ describe "#initialize" do
12
+ context "with save directory" do
13
+ it "should set the specified directory to @save_dir" do
14
+ downloader = described_class.new(tmp_dir)
15
+ expect(downloader.instance_variable_get(:@save_dir)).to eq tmp_dir
16
+ end
17
+ end
18
+
19
+ context "without save directory" do
20
+ it "should set temporary directory to @save_dir" do
21
+ downloader = described_class.new
22
+ expect(downloader.instance_variable_get(:@save_dir)).to match(/photomosaic/)
23
+ end
24
+ end
25
+ end
26
+
27
+ describe "#download_images" do
28
+ let(:image_url_list) do
29
+ [
30
+ "http://example.com/image01.jpg",
31
+ "http://example.com/image02.jpg",
32
+ "http://example.com/notfound.jpg"
33
+ ]
34
+ end
35
+
36
+ before do
37
+ stub_request(:get, %r(http://example\.com/image0\d\.jpg))
38
+ .to_return(status: 200, body: "hoge")
39
+ stub_request(:get, "http://example.com/notfound.jpg")
40
+ .to_return(status: 404)
41
+ FileUtils.rm_rf(tmp_dir) if Dir.exist?(tmp_dir)
42
+ Dir.mkdir(tmp_dir)
43
+ end
44
+
45
+ it "should download listed images to temporary directory" do
46
+ downloader.download_images(image_url_list)
47
+
48
+ %w(image01.jpg image02.jpg).each do |image|
49
+ expect(File.exist?(tmp_path(image))).to be_truthy
50
+ end
51
+ end
52
+
53
+ it "should return the path list" do
54
+ result = downloader.download_images(image_url_list)
55
+ expect(result).to match_array %w(image01.jpg image02.jpg).map { |image| tmp_path(image) }
56
+ end
57
+
58
+ after do
59
+ FileUtils.rm_rf(tmp_dir)
60
+ end
61
+ end
62
+
63
+ describe "#remove_save_dir" do
64
+ before do
65
+ Dir.mkdir(tmp_dir)
66
+ end
67
+
68
+ it "should remove save directory" do
69
+ downloader.remove_save_dir
70
+ expect(Dir.exist?(tmp_dir)).to be_falsy
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,151 @@
1
+ require "spec_helper"
2
+ require "fileutils"
3
+ require "tempfile"
4
+
5
+ module Photomosaic
6
+ describe Image do
7
+ let(:image_path) do
8
+ fixture_path("lena.png")
9
+ end
10
+
11
+ let(:image_path_list) do
12
+ [
13
+ fixture_path("lena_0.png"),
14
+ fixture_path("lena_1.png"),
15
+ fixture_path("lena_2.png"),
16
+ fixture_path("lena_3.png"),
17
+ fixture_path("lena_4.png"),
18
+ fixture_path("lena_5.png")
19
+ ]
20
+ end
21
+
22
+ context "class methods" do
23
+ describe "#create_mosaic_image" do
24
+ let(:image_list) do
25
+ 5.times.inject([]) do |image_list, _|
26
+ image_list << image_path_list.map { |path| described_class.new(path) }
27
+ image_list
28
+ end
29
+ end
30
+
31
+ let(:output_path) do
32
+ tmp_path("tiled_image.jpg")
33
+ end
34
+
35
+ before do
36
+ FileUtils.rm_rf(tmp_dir) if Dir.exist?(tmp_dir)
37
+ Dir.mkdir(tmp_dir)
38
+ end
39
+
40
+ it "should create the mosaic image" do
41
+ described_class.create_mosaic_image(image_list, output_path)
42
+ expect(File.exist?(output_path)).to be_truthy
43
+ end
44
+
45
+ after do
46
+ FileUtils.rm_rf(tmp_dir)
47
+ end
48
+ end
49
+
50
+ describe "#preprocess_image" do
51
+ it "should preprocess image" do
52
+ expect_any_instance_of(described_class).to receive(:resize!).with(100, 100, true).once
53
+ expect_any_instance_of(described_class).to receive(:posterize!).with(4).once
54
+ expect_any_instance_of(described_class).to receive(:reduce_colors!).with(8).once
55
+ described_class.preprocess_image(image_path, 100, 100, 4, 8)
56
+ end
57
+ end
58
+
59
+ describe "#resize_images" do
60
+ let(:image) do
61
+ double(:resize!)
62
+ end
63
+
64
+ let(:image_list) do
65
+ 5.times.inject([]) do |image_list, _|
66
+ image_list << [image]
67
+ image_list
68
+ end
69
+ end
70
+
71
+ it "should resize images" do
72
+ expect(image).to receive(:resize!).exactly(5).times
73
+ described_class.resize_images(image_list, 40, 20)
74
+ end
75
+ end
76
+ end
77
+
78
+ context "instance methods" do
79
+ let(:image) do
80
+ described_class.new(image_path)
81
+ end
82
+
83
+ describe "#characteristic_color" do
84
+ context "by RGB" do
85
+ it "should return characteristic color in RGB" do
86
+ characteristic_color = image.characteristic_color(:rgb)
87
+ expect(characteristic_color.red).to be_within(1).of(180)
88
+ expect(characteristic_color.green).to be_within(1).of(100)
89
+ expect(characteristic_color.blue).to be_within(1).of(107)
90
+ end
91
+ end
92
+
93
+ context "by HSV" do
94
+ it "should return characteristic color in HSV" do
95
+ characteristic_color = image.characteristic_color(:hsv)
96
+ expect(characteristic_color.hue).to eq 354
97
+ expect(characteristic_color.saturation).to be_within(0.5).of(44.7)
98
+ expect(characteristic_color.value).to be_within(0.5).of(70.0)
99
+ end
100
+ end
101
+ end
102
+
103
+ describe "#dispatch_images" do
104
+ let(:image_list) do
105
+ image_path_list.map { |path| described_class.new(path) }
106
+ end
107
+
108
+ it "should return the map of dispatched images as a 2-dimentional array" do
109
+ images = image.dispatch_images(image_list, 8, 8)
110
+ expect(images.length).to eq 64
111
+ expect(images[0].length).to eq 64
112
+ expect(images[0][0]).to be_a described_class
113
+ end
114
+ end
115
+
116
+ describe "#posterize!" do
117
+ it "should posterize itself" do
118
+ expect_any_instance_of(Magick::Image).to receive(:posterize).with(4)
119
+ expect_any_instance_of(described_class).to receive(:reload_image)
120
+ image.posterize!(4)
121
+ end
122
+ end
123
+
124
+ describe "#reduce_colors!" do
125
+ it "should reduce its colors" do
126
+ expect_any_instance_of(Magick::Image).to receive(:quantize).with(8)
127
+ expect_any_instance_of(described_class).to receive(:reload_image)
128
+ image.reduce_colors!(8)
129
+ end
130
+ end
131
+
132
+ describe "#resize!" do
133
+ context "to keep aspect ratio" do
134
+ it "should resize itself" do
135
+ expect_any_instance_of(Magick::Image).to receive(:resize_to_fit!).with(25, 50)
136
+ expect_any_instance_of(described_class).to receive(:reload_image)
137
+ image.resize!(25, 50, true)
138
+ end
139
+ end
140
+
141
+ context "not to keep aspect ratio" do
142
+ it "should resize itself" do
143
+ expect_any_instance_of(Magick::Image).to receive(:resize!).with(25, 50)
144
+ expect_any_instance_of(described_class).to receive(:reload_image)
145
+ image.resize!(25, 50, false)
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end