yearbook 0.2.3 → 0.3.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.
data/README.md CHANGED
@@ -1 +1,30 @@
1
- A wrapper around [ruby-opencv](https://github.com/ruby-opencv/ruby-opencv) and [rmagick](http://rmagick.rubyforge.org/) (which themselves are wrappers around [OpenCV](http://opencv.org/) and [ImageMagick](http://www.imagemagick.org) + [GraphicsMagick](http://www.graphicsmagick.org/)) for producing face-cropped images from a source image.
1
+ # Yearbook
2
+
3
+ A convenience for cropping and outputting images, by Dan Nguyen ([@dancow](https://twitter.com/dancow))
4
+
5
+ A very thin wrapper around [ruby-opencv](https://github.com/ruby-opencv/ruby-opencv) and [rmagick](http://rmagick.rubyforge.org/) (which themselves are wrappers around [OpenCV](http://opencv.org/) and [ImageMagick](http://www.imagemagick.org) + [GraphicsMagick](http://www.graphicsmagick.org/)) for producing face-cropped images from a source image.
6
+
7
+ This is in very-early stage...I'm using it mostly as a quickie image cropper for various projects, as well as a way to learn the cool [thor](https://github.com/erikhuda/thor) gem.
8
+
9
+ ## Installation
10
+
11
+ Load all the dependencies mentioned above (ha!)
12
+
13
+ Then:
14
+
15
+ `gem install yearbook`
16
+
17
+ ## Usage
18
+
19
+ ### In Ruby
20
+
21
+ require 'yearbook'
22
+
23
+ image = Yearbook::Image.new('path/to/somebody.jpg')
24
+ image.clip_and_print_best_face('output/to/somebodys-face.jpg')
25
+
26
+ ### Command-line tool
27
+
28
+ yearbook face george-washington.jpg
29
+ # will print out to "george-washington-face.jpg" by default
30
+
data/Rakefile CHANGED
@@ -1,5 +1,6 @@
1
1
  # encoding: utf-8
2
- $:.unshift File.dirname(File.expand_path './lib', __FILE__)
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
4
 
4
5
  require 'rubygems'
5
6
  require 'bundler'
@@ -23,6 +24,7 @@ Jeweler::Tasks.new do |gem|
23
24
  gem.email = "dansonguyen@gmail.com"
24
25
  gem.authors = ["dannguyen"]
25
26
  gem.files.exclude 'spec/fixtures/images/*.jpg' # exclude temporary directory
27
+ gem.executables << 'yearbook'
26
28
 
27
29
  # dependencies defined in Gemfile
28
30
  end
@@ -36,7 +38,6 @@ end
36
38
 
37
39
  RSpec::Core::RakeTask.new(:rcov) do |spec|
38
40
  spec.pattern = 'spec/**/*_spec.rb'
39
- spec.rcov = true
40
41
  end
41
42
 
42
43
  task :default => :spec
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.3
1
+ 0.3.0
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ require './lib/yearbook'
3
+
4
+ begin
5
+ Yearbook::CLI.start(ARGV)
6
+ end
@@ -1,7 +1,8 @@
1
1
  require "opencv"
2
2
  require 'hashie'
3
3
  require 'active_support' # I know, this makes me bad
4
- require 'yearbook/image'
4
+ require_relative 'yearbook/image'
5
+ require_relative 'yearbook/cli'
5
6
 
6
7
  include OpenCV
7
8
 
@@ -1,4 +1,6 @@
1
1
  require 'delegate'
2
+ require 'hashie'
3
+
2
4
  class AttHash < SimpleDelegator
3
5
  def initialize
4
6
  @hsh = {}
@@ -1,5 +1,7 @@
1
+ require 'active_support/core_ext/string'
2
+
1
3
  module Yearbook
2
- module Classifier
4
+ class Classifier
3
5
 
4
6
  DATA_DIR = File.expand_path('../../../data/classifiers', __FILE__ )
5
7
  DATA_FILES = {
@@ -7,20 +9,33 @@ module Yearbook
7
9
 
8
10
  }
9
11
 
12
+ attr_reader :filename
13
+
14
+ def initialize(fname)
15
+ @filename = fname
10
16
 
11
- # returns an array of detected objects
12
- def self.detect_objects(cv_image, object_type)
13
- detector = load_detector(object_type.to_sym)
17
+ @classifier = load_classifier(@filename)
18
+ end
14
19
 
15
- detector.detect_objects(cv_image)
20
+ def detect_objects(cvimg)
21
+ @classifier.detect_objects(cvimg)
16
22
  end
17
23
 
18
24
 
19
- def self.load_detector(object_type)
20
- fname = DATA_FILES[object_type]
21
- puts fname
22
- OpenCV::CvHaarClassifierCascade::load( fname )
25
+
26
+
27
+ # convenience
28
+ # will also pluralize anything
29
+ def self.of(object_type)
30
+ object_name = object_type.to_s.pluralize.to_sym
31
+
32
+ return self.new DATA_FILES[object_name]
23
33
  end
24
34
 
35
+
36
+ private
37
+ def load_classifier(fname)
38
+ OpenCV::CvHaarClassifierCascade::load(fname )
39
+ end
25
40
  end
26
41
  end
@@ -0,0 +1,26 @@
1
+ require 'thor'
2
+
3
+ module Yearbook
4
+ class CLI < Thor
5
+ desc 'hello', 'from'
6
+ def hello(from)
7
+ puts "Hello world, from: #{from}"
8
+ end
9
+
10
+ desc 'face',
11
+ %q{ Provide the path to an image with a face in it:
12
+ yearbook face /path/to/photo.jpg /path/out/face-in-photo.jpg
13
+ }
14
+ def face(path, outpath=nil)
15
+ fpath = File.expand_path(path)
16
+ @image = Image.new(fpath)
17
+
18
+ outpath ||= path.sub(/(?=\.\w*$)/, '-face')
19
+ # eh overwrite it anyway
20
+ @image.clip_and_print_best_face(outpath)
21
+ end
22
+ end
23
+
24
+
25
+ end
26
+
@@ -0,0 +1,26 @@
1
+ # NOT USED YET
2
+ # module Yearbook
3
+ # class DetectedObject
4
+
5
+ # attr_reader @x, @y, @width, @height
6
+
7
+ # # args can either be one CvMat object,
8
+ # # or x,y,w,h dimensions
9
+ # def initialize(*args)
10
+
11
+ # case args.count
12
+ # when 1
13
+ # # CVMat
14
+ # when 4
15
+ # @x = x
16
+ # @y = y
17
+ # @width = w
18
+ # @height = h
19
+ # else
20
+ # raise "Expected 1 or 4 arguments, not #{args.count}"
21
+ # end
22
+ # end
23
+
24
+
25
+ # end
26
+ # end
@@ -0,0 +1,13 @@
1
+ require_relative 'detection/collection'
2
+
3
+ module Yearbook
4
+ module Detection
5
+ end
6
+
7
+ class << self
8
+ def DetectionCollection(cvseq, obj_type = nil)
9
+ Detection::Collection.new(cvseq)
10
+ end
11
+ end
12
+ end
13
+
@@ -0,0 +1,20 @@
1
+ require 'delegate'
2
+
3
+ module Yearbook
4
+ module Detection
5
+ class Collection < SimpleDelegator
6
+ def initialize(cvseq, opts={})
7
+ @cv_seq = cvseq
8
+
9
+ super(@cv_seq)
10
+ end
11
+
12
+ # returns just one of the CvAvgComps
13
+ def best
14
+ sort_by{|c| c.width * c.height}.reverse.first
15
+ end
16
+
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ require_relative 'classifier'
2
+ require_relative 'detection'
3
+
4
+ module Yearbook
5
+ module Detector
6
+
7
+ class << self
8
+ # returns an array of detected objects
9
+ def detect_objects(cv_image, object_type)
10
+ x = Classifier.of(object_type).detect_objects(cv_image)
11
+
12
+
13
+ return Yearbook.DetectionCollection(x, object_type)
14
+ end
15
+
16
+ def load_cv(fname)
17
+ IplImage::load(fname)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,115 +1,150 @@
1
- require 'rmagick'
2
- require 'hashie'
3
1
  require_relative 'att_hash'
4
- require_relative 'classifier'
5
-
6
-
7
-
2
+ require_relative 'manipulator'
3
+ require_relative 'detector'
8
4
 
9
5
  module Yearbook
10
6
  class Image
11
7
 
12
- attr_reader :filename
8
+ attr_reader :filename, :clips
13
9
  def initialize(fname)
14
- @filename = fname
15
- @objects = []
10
+ @filename = fname
16
11
  end
17
12
 
18
13
 
19
- def detect_objects(obj_type)
20
- @objects = Classifier.detect_objects(cv_object, obj_type).to_a
14
+
15
+ # args is empty for now...
16
+ def clip(obj_type, *args, &blk)
17
+ @clips = detect_and_collect(cv_image, obj_type)
18
+ end
19
+
20
+
21
+
22
+
23
+ def clipped?; @clips.count > 0; end
24
+
25
+ # just the original image
26
+ def print(fname, &blk)
27
+ write_images(magick_image, fname, &blk)
21
28
  end
22
29
 
23
- def detect_faces
24
- detect_objects(:faces)
30
+ # print all the clips
31
+ def print_clips(fname, &blk)
32
+ write_images(constitute_clips, fname, &blk)
25
33
  end
26
34
 
27
- def detected_objects
28
- @objects
35
+ def print_best_clip(fname, &blk)
36
+ write_images(constitute_best_clip, fname, &blk)
29
37
  end
30
38
 
31
- def write(base_out_fname, &blk)
32
- klass = self.class
39
+ # convenience method
40
+ def clip_and_print(obj_type, filename, *args, &blk)
41
+ clip(obj_type, *args, &blk)
33
42
 
34
- if @objects.empty?
35
- img_objects = Array(image_object)
36
- else
37
- img_objects = @objects.map{|o| constitute_from_cv(o, image_object)}
38
- end
43
+ print_clips(filename)
44
+ end
39
45
 
40
- img_objects.each_with_index do |image_out, idx|
41
- if block_given?
42
- h = AttHash.new
43
- yield h
46
+ # convenience method
47
+ def clip_and_print_best(obj_type, filename, *args, &blk)
48
+ clip(obj_type, *args, &blk)
44
49
 
45
- # transform the image
46
- image_out = h.inject(image_out) do |img, (foo, args)|
47
- klass.send(foo, img, *args)
48
- end
49
- end
50
+ print_best_clip(filename)
51
+ end
50
52
 
51
- if idx == 0
52
- out_fname = base_out_fname
53
- else
54
- out_fname = base_out_fname.sub(/\.(?=\w+$)/, "-#{idx}.")
55
- end
56
53
 
57
- klass.output(image_out, out_fname)
58
- end
54
+ ### META STUFF
55
+
56
+ CLIPPER_REGEX = /^(clip_and_print(?:_best)?|clip)_(\w+)$/
57
+ def method_missing(foo, *args, &blk)
58
+ f = foo.to_s
59
+ # e.g. clip_faces
60
+ if f =~ CLIPPER_REGEX
61
+ self.send $1, $2, *args, &blk
62
+ else
63
+ super
64
+ end
59
65
  end
60
66
 
67
+ def respond_to?(foo, include_private=false)
68
+ return foo.to_s =~ CLIPPER_REGEX ? true : super(foo, include_private)
69
+ end
61
70
 
62
71
 
63
72
 
64
73
 
65
74
  private
66
75
 
76
+ def best_clip
77
+ @clips.best
78
+ end
67
79
 
68
- # defer loading until it is needed
69
- def image_object
70
- @magick_image ||= load_magick_image(@filename)
80
+ def constitute_from_cv(cv, img)
81
+ Manipulator.constitute(img, cv.x, cv.y, cv.width, cv.height)
71
82
  end
72
83
 
73
- def cv_object
74
- @cv_image ||= load_cv_image(@filename)
84
+ def constitute(objects)
85
+ objs = Array(objects)
86
+ objs.map{|o| constitute_from_cv(o, magick_image)}
75
87
  end
76
88
 
89
+ # just a helper
90
+ def constitute_clips
91
+ constitute(@clips)
92
+ end
77
93
 
94
+ def constitute_best_clip
95
+ constitute(best_clip)
96
+ end
78
97
 
79
- def constitute_from_cv(c, img)
80
- pixels = img.dispatch(c.x, c.y, c.width, c.height, "RGB")
98
+ def cv_image
99
+ @_cv_image ||= Detector.load_cv(@filename)
100
+ end
81
101
 
82
- return Magick::Image.constitute(c.width, c.height, "RGB", pixels)
102
+ def detect_and_collect(cv, obj_type)
103
+ Detector.detect_objects(cv, obj_type)
104
+ end
105
+
106
+ # defer loading until it is needed
107
+ def magick_image
108
+ @_magick_image ||= Manipulator.load_magick(@filename)
83
109
  end
84
110
 
85
111
 
86
- def load_magick_image(fname)
87
- Magick::Image::read(fname).first
88
- end
112
+ # returns a manipulated Magick file
113
+ def transform_image(obj, &blk)
114
+ transformed_obj = obj
115
+ if block_given?
116
+ method_queue = AttHash.new
117
+ yield method_queue
89
118
 
119
+ # transform the image
120
+ transformed_obj = method_queue.inject(transformed_obj) do |img, (foo, args)|
121
+ Manipulator.send(foo, img, *args)
122
+ end
123
+ end
90
124
 
91
- def load_cv_image(fname)
92
- IplImage::load(fname)
125
+ return transformed_obj
93
126
  end
94
127
 
128
+ def write_images(magick_objects, base_out_fname, &blk)
95
129
 
96
- # Image manipulation methods at the class level
97
- # no reason for them to be instance methods
98
- class << self
99
- def bw(img, num_colors = 128)
100
- img.quantize(num_colors, Magick::GRAYColorspace)
101
- end
130
+ arr = Array(magick_objects)
102
131
 
103
- def resize_to_fit(img, w, h = nil)
104
- h ||= w
132
+ arr.each_with_index do |obj, idx|
133
+ output_img = transform_image(obj, &blk)
134
+ # generate a numbered filename if there are more than one
135
+ output_fname = arr.count == 1 ? base_out_fname : base_out_fname.sub(/\.(?=\w+$)/, "-#{idx}.")
105
136
 
106
- img.resize_to_fit(w, h)
137
+ write_image(output_img, output_fname)
107
138
  end
139
+ end
108
140
 
109
- def output(img, out_fname)
110
- img.write(out_fname)
111
- end
141
+
142
+ # img is expected to be a Magick Object
143
+ def write_image(img, out_fname)
144
+ img.write(out_fname)
112
145
  end
113
146
 
147
+
148
+
114
149
  end
115
150
  end
@@ -0,0 +1,33 @@
1
+ require 'rmagick'
2
+
3
+ module Yearbook
4
+ module Manipulator
5
+
6
+ class << self
7
+ def bw(img, num_colors = 128)
8
+ img.quantize(num_colors, Magick::GRAYColorspace)
9
+ end
10
+
11
+
12
+ def constitute(img, *args)
13
+ x,y,w,h = args[0..3]
14
+ pixels = img.dispatch(x, y, w, h, "RGB")
15
+
16
+ Magick::Image.constitute(w, h, "RGB", pixels)
17
+ end
18
+
19
+ def resize_to_fit(img, w, h = nil)
20
+ h ||= w
21
+
22
+ img.resize_to_fit(w, h)
23
+ end
24
+
25
+
26
+ def load_magick(fname)
27
+ Magick::Image::read(fname).first
28
+ end
29
+
30
+
31
+ end
32
+ end
33
+ end
@@ -21,39 +21,126 @@ module Yearbook
21
21
  end
22
22
  end
23
23
 
24
- describe '#detect_faces' do
25
- it 'should return an Array' do
26
- expect( @image.detect_faces ).to be_an Array
24
+ context 'clipping' do
25
+ before(:each) do
26
+ @image.clip_faces
27
+ end
28
+
29
+ it 'should be #clipped?' do
30
+ expect(@image).to be_clipped
31
+ end
32
+
33
+ describe '@clips' do
34
+ it 'should return an Detection::Collection' do
35
+ expect( @image.clips ).to be_a Yearbook::Detection::Collection
36
+ end
27
37
  end
28
38
  end
29
39
 
30
- describe '#write' do
31
- it 'should write to disk' do
32
- @image.write(@tempfile.path)
33
- expect(@tempfile.size > 10000).to be_true
40
+ describe 'printing' do
41
+ describe '#print' do
42
+ it 'should write the same file to disk' do
43
+ @image.print(@tempfile.path)
44
+ expect(@tempfile.size > 10000).to be_true
45
+ end
34
46
  end
35
47
 
36
- context 'after faces have been detected' do
48
+ describe '#print_clips' do
49
+ context 'after faces have been clipped' do
50
+ before do
51
+ @image.clip_faces
52
+ end
53
+
54
+ it 'should print each object to disk' do
55
+ temp_path = @tempfile.path
56
+ @image.print_clips(temp_path)
57
+ @image.clips.each_with_index do |idx|
58
+ clip_path = temp_path.sub(/\.(?=\w+$)/, "-#{idx}.")
59
+ expect(File.exists?(clip_path)).to be_true
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ describe '#print_best_clip' do
37
66
  before do
38
- @image.detect_faces
67
+ @image.clip_faces
68
+ @image.print_best_clip(@tempfile.path)
69
+ end
70
+
71
+ it 'should print the one face' do
72
+ expect(File.exists?@tempfile.path).to be_true
39
73
  end
40
74
 
41
- it 'should write each object to disk' do
42
- last_num = @image.detected_objects.count - 1
43
- path = @tempfile.path
44
- last_path = path.sub(/\.(?=\w+$)/, "-#{last_num}.")
75
+ it 'that face should be the biggest face' do
76
+ best_face = @image.send(:best_clip)
77
+
78
+ # icky method, requires using best_clip private method
79
+ # AND knowing default Detection::Collection :best sorting
45
80
 
46
- @image.write(path)
47
- expect(File.exists?(last_path)).to be_true
81
+ pixel_count = best_face.width * best_face.height
82
+ magick_image = Manipulator.load_magick(@tempfile.path)
83
+
84
+ expect(pixel_count).to eq magick_image.columns * magick_image.rows
85
+ end
86
+ end
87
+
88
+ context 'meta-conveniences' do
89
+ it 'should have #clip_and_print_faces' do
90
+ pending 'better path'
91
+ @image.clip_and_print_faces(@tempfile.path)
92
+ end
93
+
94
+ it 'should have #clip_and_print_best_face' do
95
+ # note how tolerant it is of singular things
96
+ @image.clip_and_print_best_face(@tempfile.path)
97
+ best_face = @image.send(:best_clip)
98
+
99
+ # redux test
100
+ # icky method, requires using best_clip private method
101
+ # AND knowing default Detection::Collection :best sorting
102
+
103
+ pixel_count = best_face.width * best_face.height
104
+ magick_image = Manipulator.load_magick(@tempfile.path)
105
+
106
+
107
+ binding.pry
108
+ expect(pixel_count).to eq magick_image.columns * magick_image.rows
48
109
  end
49
110
 
50
111
  end
51
112
  end
113
+
114
+ context 'cropping' do
115
+ it 'should allow close cropping'
116
+ it 'should take in percentages and weights'
117
+ # crop_to_object -10, -20
118
+ end
52
119
 
53
120
 
121
+ it 'should' do
122
+ pending 'what'
123
+ @image.clipped? # to eq true
124
+ @image.clips # to be an array
125
+ @image.clips # to eq CV thingy
54
126
 
55
- end
127
+ # # API:
128
+ # img = @image.new(@fname)
129
+ # img.clip_face
130
+ # img.write do |f|
131
+ # # etc do |f|
132
+ # f.clip_face # -d face
133
+ # f.bw # --bw
134
+ # f.resize_to_fill(100, 200) # --fill 100,200
135
+ # f.write(whatev)
136
+ # end
56
137
 
138
+ # yearbook photo.jpg face.jpg --clip face
139
+
140
+ end
57
141
 
142
+
143
+
144
+ end
58
145
  end
59
146
  end
@@ -5,13 +5,14 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "yearbook"
8
- s.version = "0.2.3"
8
+ s.version = "0.3.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["dannguyen"]
12
- s.date = "2013-11-06"
12
+ s.date = "2013-11-09"
13
13
  s.description = "Easy face-cropping"
14
14
  s.email = "dansonguyen@gmail.com"
15
+ s.executables = ["yearbook", "yearbook"]
15
16
  s.extra_rdoc_files = [
16
17
  "LICENSE.txt",
17
18
  "README.md"
@@ -25,6 +26,7 @@ Gem::Specification.new do |s|
25
26
  "README.md",
26
27
  "Rakefile",
27
28
  "VERSION",
29
+ "bin/yearbook",
28
30
  "data/classifiers/eye-left-ojoI.xml",
29
31
  "data/classifiers/frontalEyes35x16.xml",
30
32
  "data/classifiers/haarcascade_frontalface_alt.xml",
@@ -34,7 +36,13 @@ Gem::Specification.new do |s|
34
36
  "lib/yearbook.rb",
35
37
  "lib/yearbook/att_hash.rb",
36
38
  "lib/yearbook/classifier.rb",
39
+ "lib/yearbook/cli.rb",
40
+ "lib/yearbook/detected_object.rb",
41
+ "lib/yearbook/detection.rb",
42
+ "lib/yearbook/detection/collection.rb",
43
+ "lib/yearbook/detector.rb",
37
44
  "lib/yearbook/image.rb",
45
+ "lib/yearbook/manipulator.rb",
38
46
  "spec/spec_helper.rb",
39
47
  "spec/yearbook_spec.rb",
40
48
  "yearbook.gemspec"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yearbook
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-11-06 00:00:00.000000000 Z
12
+ date: 2013-11-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rmagick
@@ -157,7 +157,8 @@ dependencies:
157
157
  version: '0'
158
158
  description: Easy face-cropping
159
159
  email: dansonguyen@gmail.com
160
- executables: []
160
+ executables:
161
+ - yearbook
161
162
  extensions: []
162
163
  extra_rdoc_files:
163
164
  - LICENSE.txt
@@ -171,6 +172,7 @@ files:
171
172
  - README.md
172
173
  - Rakefile
173
174
  - VERSION
175
+ - bin/yearbook
174
176
  - data/classifiers/eye-left-ojoI.xml
175
177
  - data/classifiers/frontalEyes35x16.xml
176
178
  - data/classifiers/haarcascade_frontalface_alt.xml
@@ -180,7 +182,13 @@ files:
180
182
  - lib/yearbook.rb
181
183
  - lib/yearbook/att_hash.rb
182
184
  - lib/yearbook/classifier.rb
185
+ - lib/yearbook/cli.rb
186
+ - lib/yearbook/detected_object.rb
187
+ - lib/yearbook/detection.rb
188
+ - lib/yearbook/detection/collection.rb
189
+ - lib/yearbook/detector.rb
183
190
  - lib/yearbook/image.rb
191
+ - lib/yearbook/manipulator.rb
184
192
  - spec/spec_helper.rb
185
193
  - spec/yearbook_spec.rb
186
194
  - yearbook.gemspec
@@ -199,7 +207,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
199
207
  version: '0'
200
208
  segments:
201
209
  - 0
202
- hash: 1267896751848541700
210
+ hash: 277460540779661417
203
211
  required_rubygems_version: !ruby/object:Gem::Requirement
204
212
  none: false
205
213
  requirements: