yearbook 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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: