├── lib ├── facepp.rb └── facepp │ └── client.rb ├── .gitignore ├── ..gemspec ├── facepp.gemspec ├── README.md ├── LICENSE └── spec └── facepp_spec.rb /lib/facepp.rb: -------------------------------------------------------------------------------- 1 | require 'facepp/client' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /..gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/./version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ["Ray Song"] 6 | gem.email = ["i@maskray.me"] 7 | gem.description = %q{TODO: Write a gem description} 8 | gem.summary = %q{TODO: Write a gem summary} 9 | gem.homepage = "" 10 | 11 | gem.files = `git ls-files`.split($\) 12 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 13 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 14 | gem.name = "." 15 | gem.require_paths = ["lib"] 16 | gem.version = .::VERSION 17 | end 18 | -------------------------------------------------------------------------------- /facepp.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'facepp' 3 | s.version = '0.4.1' 4 | s.platform = Gem::Platform::RUBY 5 | s.summary = 'A Ruby interface to the FacePlusPlus API.' 6 | s.description = 'FacePlusPlus API Reference: http://faceplusplus.com/en/docs/getting_started' 7 | s.homepage = 'http://faceplusplus.com/en/docs/download/sdk' 8 | s.licenses = ['MIT'] 9 | s.authors = ['SONG Fangrui', 'Matt Sanford'] 10 | s.email = ['sfr@megvii.com', 'matt@pushd.com'] 11 | s.files = ['LICENSE'] 12 | s.files += Dir.glob 'lib/**/*.rb' 13 | s.files += Dir.glob 'spec/**/*' 14 | s.require_paths = ['lib'] 15 | 16 | s.add_development_dependency 'rspec' 17 | end 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FacePlusPlus Ruby SDK 2 | 3 | A Ruby interface to the FacePlusPlus API. 4 | 5 | ## Supported Ruby Versions 6 | 7 | This sdk has been tested against the following Ruby versions: 8 | 9 | - 1.8.7_p370 10 | - 1.9.3_p286 11 | 12 | ## Installation 13 | 14 | ```bash 15 | gem build facepp.gemspec 16 | gem install *.gem 17 | ``` 18 | 19 | ## Get Started 20 | 21 | ```ruby 22 | require 'facepp' 23 | 24 | api = FacePP.new 'YOUR_API_KEY', 'YOUR_API_SECRET' 25 | puts api.detection.detect url: '/tmp/0.jpg' 26 | puts api.person.create person_name: 'Curry' 27 | puts api.person.add_faces person_name: 'Curry', face_id: ['FACD_ID_0', 'FACE_ID_1'] 28 | puts api.person.add_faces person_name: 'Curry', face_id: ['FACD_ID_0', 'FACE_ID_1'].to_set 29 | puts api.person.add_faces person_id: 'PERSON_ID', face_id: 'FACE_ID' 30 | puts api.info.get_quota 31 | ``` 32 | 33 | See the RSpec tests for more examples. 34 | 35 | ## License 36 | 37 | Licensed under the MIT license. 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 SONG Fangrui 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /spec/facepp_spec.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | require 'facepp' 3 | 4 | RSpec::Matchers.define :be_person do 5 | match do |actual| 6 | actual.should have_key 'person_id' 7 | end 8 | end 9 | 10 | describe FacePP do 11 | let(:api) { FacePP.new 'YOUR_API_KEY', 'YOUR_API_SECRET' } 12 | let(:face_id1) { api.detection.detect(img: '/tmp/0.jpg')['face'][0]['face_id'] } 13 | let(:face_id2) { api.detection.detect(img: '/tmp/1.jpg')['face'][0]['face_id'] } 14 | 15 | describe 'recognition' do 16 | it 'can compare' do 17 | #puts 'api: ', api 18 | #puts face_id1, ' ', face_id2 19 | #puts api.detection.detect(img: '/tmp/0.jpg')['face'][0]['face_id'] 20 | 21 | res = api.recognition.compare face_id1: face_id1, face_id2: face_id2 22 | res.should have_key 'component_similarity' 23 | end 24 | end 25 | 26 | describe 'person' do 27 | let(:name) { "Haskell-#{rand 10}" } 28 | 29 | before :all do 30 | res = api.person.create(person_name: name) 31 | res.should be_person 32 | @person_id = res['person_id'] 33 | end 34 | 35 | it 'can get_info' do 36 | api.person.get_info(person_id: @person_id).should be_person 37 | api.person.get_info(person_name: name).should be_person 38 | end 39 | 40 | it 'can add_face' do 41 | api.person.add_face(person_id: @person_id, face_id: face_id1).should have_key 'success' 42 | end 43 | 44 | it 'can remove_face' do 45 | api.person.remove_face(person_id: @person_id, face_id: face_id1).should have_key 'success' 46 | end 47 | 48 | it 'can add_face in chunks' do 49 | api.person.add_face(person_id: @person_id, face_id: [face_id1, face_id2]).should have_key 'success' 50 | end 51 | 52 | it 'can remove_face in chunks' do 53 | api.person.remove_face(person_id: @person_id, face_id: [face_id1, face_id2].to_set).should have_key 'success' 54 | end 55 | 56 | after :all do 57 | api.person.delete(person_name: name).should have_key 'success' 58 | end 59 | end 60 | 61 | describe 'info' do 62 | it 'can get_group_list' do 63 | res = api.info.get_group_list 64 | res.should have_key 'group' 65 | end 66 | 67 | it 'can get_quota' do 68 | res = api.info.get_quota 69 | res.should have_key 'QUOTA_ALL' 70 | res.should have_key 'QUOTA_SEARCH' 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/facepp/client.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'json' 3 | require 'mime/types' 4 | require 'net/http' 5 | require 'securerandom' 6 | require 'uri' 7 | 8 | unless Object.respond_to? :singleton_class 9 | class Object 10 | def singleton_class 11 | class << self; self; end 12 | end 13 | end 14 | end 15 | 16 | unless Object.respond_to? :define_singleton_method 17 | class Object 18 | def define_singleton_method name, &block 19 | singleton_class.send :define_method, name, &block 20 | end 21 | end 22 | end 23 | 24 | unless URI.respond_to? :encode_www_form 25 | module URI 26 | TBLENCWWWCOMP_ = {} # :nodoc: 27 | 256.times do |i| 28 | TBLENCWWWCOMP_[i.chr] = '%%%02X' % i 29 | end 30 | TBLENCWWWCOMP_[' '] = '+' 31 | TBLENCWWWCOMP_.freeze 32 | 33 | def self.encode_www_form_component str 34 | str.to_s.gsub(/[^*\-.0-9A-Z_a-z]/) {|c| TBLENCWWWCOMP_[c] } 35 | end 36 | 37 | def self.encode_www_form enum 38 | enum.map {|k,v| "#{encode_www_form_component k}=#{encode_www_form_component v}" }.join '&' 39 | end 40 | end 41 | end 42 | 43 | class FacePP 44 | 45 | class HttpException < StandardError 46 | def initialize(response) 47 | @response = response 48 | end 49 | 50 | def code 51 | @response.code 52 | end 53 | 54 | def to_s 55 | "HTTP #{code}: #{@response.inspect}" 56 | end 57 | end 58 | 59 | class MultiPart 60 | def initialize 61 | @fields = [] 62 | @files = [] 63 | @boundary = [SecureRandom.random_bytes(15)].pack('m*').chop! 64 | end 65 | 66 | def add_field name, value 67 | @fields << [name, value] 68 | end 69 | 70 | def add_file name, filepath 71 | @files << [name, filepath] 72 | end 73 | 74 | def self.guess_mime filename 75 | res = MIME::Types.type_for(filename) 76 | res.empty? ? 'application/octet-stream' : res[0] 77 | end 78 | 79 | def has_file? 80 | not @files.empty? 81 | end 82 | 83 | def content_type 84 | "multipart/form-data; boundary=#{@boundary}" 85 | end 86 | 87 | def inspect 88 | res = StringIO.new 89 | append_boundary = lambda { res.write "--#{@boundary}\r\n" } 90 | @fields.each do |field| 91 | append_boundary[] 92 | res.write "Content-Disposition: form-data; name=\"#{field[0]}\"\r\n\r\n#{field[1]}\r\n" 93 | end 94 | @files.each do |file| 95 | append_boundary[] 96 | res.write "Content-Disposition: file; name=\"#{file[0]}\"; filename=\"#{file[1]}\"\r\n" 97 | res.write "Content-Type: #{self.class.guess_mime file[1]}\r\n" 98 | res.write "Content-Transfer-Encoding: binary\r\n\r\n" 99 | res.write File.open(file[1]).read 100 | res.write "\r\n" 101 | end 102 | res.write "--#{@boundary}--\r\n" 103 | res.rewind 104 | res.read 105 | end 106 | end 107 | 108 | APIS = [ 109 | '/detection/detect', 110 | '/detection/landmark', 111 | 112 | '/info/get_image', 113 | '/info/get_face', 114 | '/info/get_session', 115 | '/info/get_quota', 116 | '/info/get_person_list', 117 | '/info/get_group_list', 118 | '/info/get_app', 119 | 120 | '/faceset/create', 121 | '/faceset/delete', 122 | '/faceset/add_face', 123 | '/faceset/remove_face', 124 | '/faceset/set_info', 125 | '/faceset/get_info', 126 | 127 | '/person/create', 128 | '/person/delete', 129 | '/person/add_face', 130 | '/person/remove_face', 131 | '/person/get_info', 132 | '/person/set_info', 133 | 134 | '/train/verify', 135 | '/train/search', 136 | '/train/identify', 137 | 138 | '/grouping/grouping', 139 | 140 | '/group/create', 141 | '/group/delete', 142 | '/group/add_person', 143 | '/group/remove_person', 144 | '/group/get_info', 145 | '/group/set_info', 146 | 147 | '/recognition/compare', 148 | '/recognition/train', 149 | '/recognition/verify', 150 | '/recognition/recognize', 151 | '/recognition/search', 152 | ] 153 | 154 | def initialize(key, secret, options={}) 155 | decode = options.fetch :decode, true 156 | api_host = options.fetch :host, 'api.faceplusplus.com' 157 | make_hash = lambda { Hash.new {|h,k| h[k] = make_hash.call make_hash } } 158 | 159 | APIS.each do |api| 160 | m = self 161 | breadcrumbs = api.split('/')[1..-1] 162 | breadcrumbs[0..-2].each do |breadcrumb| 163 | unless m.instance_variable_defined? "@#{breadcrumb}" 164 | m.instance_variable_set "@#{breadcrumb}", Object.new 165 | m.singleton_class.class_eval do 166 | attr_reader breadcrumb 167 | end 168 | end 169 | m = m.instance_variable_get "@#{breadcrumb}" 170 | end 171 | 172 | m.define_singleton_method breadcrumbs[-1] do |*args| 173 | form = MultiPart.new 174 | fields = {'api_key' => key, 'api_secret' => secret} 175 | (args[0] || {}).each do |k,v| 176 | if k.to_s == 'img' # via POST 177 | form.add_file k, v 178 | else 179 | fields[k] = v.is_a?(Enumerable) ? v.to_a.join(',') : v 180 | end 181 | end 182 | 183 | req = Net::HTTP::Post.new("#{api}?#{URI::encode_www_form(fields)}") 184 | if form.has_file? 185 | req.set_content_type form.content_type 186 | req.body = form.inspect 187 | req['Content-Length'] = req.body.size 188 | end 189 | 190 | response = Net::HTTP.new(api_host).request(req) 191 | if !response.kind_of?(Net::HTTPSuccess) 192 | raise(HttpException.new(response)) 193 | end 194 | 195 | decode ? JSON.load(response.body) : response.body 196 | end 197 | end 198 | end 199 | end 200 | --------------------------------------------------------------------------------