matroid 0.0.1 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/Gemfile +0 -1
- data/README.md +112 -8
- data/lib/matroid/connection.rb +148 -0
- data/lib/matroid/detector.rb +293 -0
- data/lib/matroid/error.rb +12 -0
- data/lib/matroid/version.rb +1 -1
- data/lib/matroid.rb +72 -33
- data/matroid.gemspec +12 -1
- metadata +137 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c7bcfdbc2cb07e551d37703c5a5468151c64b4df
|
4
|
+
data.tar.gz: b5375b70f1fd8e3e7fb6575f72ba6befde09764c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d1185974ff919bd69527b1fdbc361578af8aef7ae1551e7742ab088c2740361f027f44ee982877741a65a1bafb08de0dcc12c32c9bba3c4c86c9273f92d05d83
|
7
|
+
data.tar.gz: ddafcc63bcce5582ee7a3a71d13787cfc9ce70c562466db077ce220fcd588e33ba312554862f330c64044cec4ad4a50830e0def32d220598bfd69910a45f6dc2
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,8 +1,7 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
1
|
+
## Features
|
2
|
+
* API endpoint coverage
|
3
|
+
* Automatic authenticated work flow
|
4
|
+
* [Full documentation](http://www.rubydoc.info/github/matroid/matroid-ruby)
|
6
5
|
|
7
6
|
## Installation
|
8
7
|
|
@@ -21,11 +20,116 @@ Or install it yourself as:
|
|
21
20
|
$ gem install matroid
|
22
21
|
|
23
22
|
## Usage
|
23
|
+
This API wrapper allows you to easily create and use Matroid detectors for classifying various media.
|
24
|
+
It is designed to allow you to use detectors without any notion of the API.
|
25
|
+
|
26
|
+
Check the [documentation](http://www.rubydoc.info/github/matroid/matroid-ruby) for the complete reference of available methods.
|
27
|
+
|
28
|
+
# Authenticate your session
|
29
|
+
The Matroid API relies on the use of access tokens.
|
30
|
+
The easiest way to automatically authenticate your usage and handle access tokens
|
31
|
+
is to declare `MATROID_CLIENT_ID` and `MATROID_CLIENT_SECRET` in your environment.
|
32
|
+
For example, place the following in your `.env` file (this library includes the `dotenv` gem):
|
33
|
+
```
|
34
|
+
MATROID_CLIENT_ID=XXXXXXXXXXXXXXXXXXX
|
35
|
+
MATROID_CLIENT_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXX
|
36
|
+
```
|
37
|
+
Also, you may call `Matroid.authenticate(MATROID_CLIENT_ID, MATROID_CLIENT_SECRET)` before
|
38
|
+
any other methods and the token will be stored in the instance and refreshed as needed.
|
39
|
+
|
40
|
+
# Example API usage
|
41
|
+
### Authentication
|
42
|
+
```ruby
|
43
|
+
require 'matroid'
|
44
|
+
|
45
|
+
MATROID_CLIENT_ID=XXXXXXXXXXXXXXXXXXX
|
46
|
+
MATROID_CLIENT_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXX
|
47
|
+
|
48
|
+
Matroid.authenticate(MATROID_CLIENT_ID, MATROID_CLIENT_SECRET)
|
49
|
+
|
50
|
+
# Check user account info like Matroid Credits balance
|
51
|
+
Matroid.account_info
|
52
|
+
```
|
53
|
+
|
54
|
+
### Query for Detectors
|
55
|
+
```ruby
|
56
|
+
# Get detector by id
|
57
|
+
detector = Matroid::Detector.find_by_id('5893f98530c1c00d0063835b')
|
58
|
+
|
59
|
+
# Get detectors by query
|
60
|
+
# Search by one or more of :labels, :name, :state, :id, :permission_level, :owner, :detector_type
|
61
|
+
# :labels and :name queries can be String or Regexp
|
62
|
+
detector = Matroid::Detector.find(id: '5893f98530c1c00d0063835b').first
|
63
|
+
cat_detectors = Matroid::Detector.find(name: 'cat')
|
64
|
+
cat_detector_id = Matroid::Detector.find(labels: 'cat', owner: true, state: 'trained').first.id
|
24
65
|
|
25
|
-
|
66
|
+
# Find published detectors
|
67
|
+
cat_detectors = Matroid::Detector.find(name: 'cat', published: true)
|
68
|
+
cat_detector_id = Matroid::Detector.find(labels: 'cat', state: 'trained', published: true).first.id
|
69
|
+
|
70
|
+
# convenience methods
|
71
|
+
# .find_by_id(String)
|
72
|
+
# .find_one(Hash)
|
73
|
+
# .find_by_<attribute>(String)
|
74
|
+
# .find_one_by_<attribute>(String)
|
75
|
+
|
76
|
+
# View cached detectors from previous searches
|
77
|
+
all_detectors = Matroid::Detector.cached
|
78
|
+
```
|
79
|
+
|
80
|
+
### Use Detector class methods
|
81
|
+
```ruby
|
82
|
+
# Get detector details
|
83
|
+
detector.to_hash #=> Hash of all the details (or you can get them separately as below)
|
84
|
+
detector.info #=> displays detector attributes in a nice printout
|
85
|
+
|
86
|
+
detector.id #=> "5893f98530c1c00d0063835b"
|
87
|
+
detector.name #=> "My cool detector"
|
88
|
+
detector.state #=> "trained"
|
89
|
+
detector.labels #=> ["label 1", "label 2", ...]
|
90
|
+
detector.permission_level #=> "private"
|
91
|
+
detector.owner #=> true
|
92
|
+
detector.training #=> "successful"
|
93
|
+
detector.type #=> "object", "face", "facial_characteristics"
|
94
|
+
|
95
|
+
# Create a detector
|
96
|
+
detector = Matroid::Detector.create('PATH/TO/ZIP/FILE', 'My awesome detector', 'general') # uploads labels and images
|
97
|
+
detector.id #=> "XxXxXxXxXxXxXxXxXxXxXxXxXxXxXx"
|
98
|
+
detector.name #=> "My awesome detector"
|
99
|
+
detector.state #=> "pending"
|
100
|
+
detector.labels #=> ["label 1", "label 2", ...]
|
101
|
+
detector.permission_level #=> "private"
|
102
|
+
detector.owner #=> true
|
103
|
+
detector.type #=> "general"
|
104
|
+
detector.train # submits the detector for training
|
105
|
+
# You can repeatedly call detector.info to get the updates on the training
|
106
|
+
|
107
|
+
# Use a detector
|
108
|
+
detector = Matroid::Detector.find_by_id('5893f98530c1c00d0063835b')
|
109
|
+
|
110
|
+
# Classifying an image returns a hash of the detected labels (with probabilities)
|
111
|
+
# along with bounding box information (if applicable)
|
112
|
+
image_file_path = 'PATH/TO/IMAGE/FILE'
|
113
|
+
image_url = 'https://www.example.com/images/some_image.jpg'
|
114
|
+
detector.classify_image_url(image_url)
|
115
|
+
detector.classify_image_file(image_file_path)
|
116
|
+
|
117
|
+
# Classifying a video returns a hash { "video_id" => "dfoguhd078yd7dg87dfvsdf7" }
|
118
|
+
# which can later be used to check on the classification.
|
119
|
+
# A video takes some time to classify depending on the length and size of the video uploaded.
|
120
|
+
youtube_url = 'https://www.youtube.com/watch?v=0qVOUD76JOg'
|
121
|
+
video_file_path = 'PATH/TO/VIDEO/FILE'
|
122
|
+
detector.classify_video_url(youtube_url)
|
123
|
+
detector.classify_video_file(video_file_path)
|
124
|
+
|
125
|
+
# Call the following repeatedly to check the progress on the video classification results
|
126
|
+
detector_id = 'dfoguhd078yd7dg87dfvsdf7'
|
127
|
+
Matroid.get_video_results(detector_id) #=> details of timestamps with labels, etc.
|
128
|
+
|
129
|
+
```
|
130
|
+
|
131
|
+
More functionality coming soon.
|
26
132
|
|
27
133
|
## Development
|
28
134
|
|
29
135
|
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
-
|
31
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'json'
|
3
|
+
require 'httpclient/webagent-cookie' # stops warning found here: https://github.com/nahi/httpclient/issues/252
|
4
|
+
require 'httpclient'
|
5
|
+
require 'date'
|
6
|
+
|
7
|
+
BASE_API_URI = 'https://www.matroid.com/api/0.1/'
|
8
|
+
DEFAULT_GRANT_TYPE = 'client_credentials'
|
9
|
+
TOKEN_RESOURCE = 'oauth/token'
|
10
|
+
VERBS = %w(get post)
|
11
|
+
|
12
|
+
module Matroid
|
13
|
+
|
14
|
+
# @attr_reader [Token] The current stored token object
|
15
|
+
class << self
|
16
|
+
attr_reader :token, :base_api_uri, :client
|
17
|
+
|
18
|
+
# Changes the default base api uri. This is used primarily for testing purposes.
|
19
|
+
# @param uri [String]
|
20
|
+
def set_base_uri(uri)
|
21
|
+
@base_api_uri = uri
|
22
|
+
end
|
23
|
+
|
24
|
+
VERBS.each do |verb|
|
25
|
+
define_method verb do |endpoint, *params|
|
26
|
+
send_request(verb, endpoint, *params)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def parse_response(response)
|
31
|
+
if valid_json?(response.body)
|
32
|
+
status = response.status_code
|
33
|
+
parsed_response = JSON.parse(response.body)
|
34
|
+
if status != 200
|
35
|
+
err_msg = JSON.pretty_generate(parsed_response)
|
36
|
+
raise Error::RateLimitError.new(err_msg) if status == 429
|
37
|
+
raise Error::InvalidQueryError.new(err_msg) if status == 422
|
38
|
+
raise Error::PaymentError.new(err_msg) if status == 402
|
39
|
+
raise Error::ServerError.new(err_msg) if status / 100 == 5
|
40
|
+
raise Error::APIError.new(err_msg)
|
41
|
+
end
|
42
|
+
|
43
|
+
parsed_response
|
44
|
+
else
|
45
|
+
p response
|
46
|
+
raise Error::APIError.new(response.body)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def get_token(client_id, client_secret)
|
53
|
+
@base_api_uri = BASE_API_URI if @base_api_uri.nil?
|
54
|
+
url = @base_api_uri + TOKEN_RESOURCE
|
55
|
+
params = auth_params(client_id, client_secret)
|
56
|
+
@client = HTTPClient.new
|
57
|
+
response = @client.post(url, body: params)
|
58
|
+
return false unless response.status_code == 200
|
59
|
+
@token = Token.new(JSON.parse(response.body))
|
60
|
+
@client.base_url = @base_api_uri
|
61
|
+
@client.default_header = { 'Authorization' => @token.authorization_header }
|
62
|
+
end
|
63
|
+
|
64
|
+
def send_request(verb, path, params = {})
|
65
|
+
path = URI.escape(path)
|
66
|
+
|
67
|
+
# refreshes token with each call
|
68
|
+
authenticate
|
69
|
+
|
70
|
+
case verb
|
71
|
+
when 'get'
|
72
|
+
response = @client.get(path, body: params)
|
73
|
+
when 'post'
|
74
|
+
response = @client.post(path, body: params)
|
75
|
+
end
|
76
|
+
|
77
|
+
parse_response(response)
|
78
|
+
end
|
79
|
+
|
80
|
+
def valid_json?(json)
|
81
|
+
begin
|
82
|
+
JSON.parse(json)
|
83
|
+
return true
|
84
|
+
rescue JSON::ParserError => e
|
85
|
+
return false
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def auth_params(client_id, client_secret)
|
90
|
+
{
|
91
|
+
'client_id' => client_id,
|
92
|
+
'client_secret' => client_secret,
|
93
|
+
'grant_type' => DEFAULT_GRANT_TYPE
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
def environment_variables?
|
98
|
+
!ENV['MATROID_CLIENT_ID'].nil? && !ENV['MATROID_CLIENT_SECRET'].nil?
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
# Represents an OAuth access token
|
104
|
+
# @attr [String] token_type ex: "Bearer"
|
105
|
+
# @attr [String] token_str The actual access token
|
106
|
+
# @attr [DateTime] born When the token was created
|
107
|
+
# @attr [String] lifetime Seconds until token expired
|
108
|
+
class Token
|
109
|
+
attr_reader :born, :lifetime, :acces_token
|
110
|
+
def initialize(options = {})
|
111
|
+
@token_type = options['token_type']
|
112
|
+
@access_token = options['access_token']
|
113
|
+
@born = DateTime.now
|
114
|
+
@lifetime = options['expires_in']
|
115
|
+
end
|
116
|
+
|
117
|
+
def authorization_header
|
118
|
+
"#{@token_type} #{@access_token}"
|
119
|
+
end
|
120
|
+
|
121
|
+
# Checks if the current token is expired
|
122
|
+
# @return [Boolean]
|
123
|
+
def expired?
|
124
|
+
lifetime_in_days = time_in_seconds(@lifetime)
|
125
|
+
@born + lifetime_in_days < DateTime.now
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
# @return [Numeric] Time left before token expires (in seconds).
|
130
|
+
def time_remaining
|
131
|
+
lifetime_in_days = time_in_seconds(@lifetime)
|
132
|
+
remaining = lifetime_in_days - (DateTime.now - @born)
|
133
|
+
remaining > 0 ? time_in_seconds(remaining) : 0
|
134
|
+
end
|
135
|
+
|
136
|
+
def time_in_seconds(t)
|
137
|
+
t * 24.0 * 60 * 60
|
138
|
+
end
|
139
|
+
|
140
|
+
def to_s
|
141
|
+
JSON.pretty_generate({
|
142
|
+
access_token: @access_token,
|
143
|
+
born: @born,
|
144
|
+
lifetime: @lifetime
|
145
|
+
})
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,293 @@
|
|
1
|
+
# size limit constants
|
2
|
+
IMAGE_FILE_SIZE_LIMIT = 50 * 1024 * 1024
|
3
|
+
VIDEO_FILE_SIZE_LIMIT = 300 * 1024 * 1024
|
4
|
+
BATCH_FILE_SIZE_LIMIT = 50 * 1024 * 1024
|
5
|
+
ZIP_FILE_SIZE_LIMIT = 300 * 1024 * 1024
|
6
|
+
SEARCH_PARAMETERS = %w(name label permission_level owner training type state)
|
7
|
+
module Matroid
|
8
|
+
|
9
|
+
|
10
|
+
# Represents a Matroid detector
|
11
|
+
# @attr [String] id Detector id
|
12
|
+
# @attr [String] name Detector name
|
13
|
+
# @attr [Array<Hash><String>] labels
|
14
|
+
# @attr [String] permission_level 'private', 'readonly', 'open', 'stock'
|
15
|
+
# @attr [Bool] owner is the current authicated user the owner
|
16
|
+
class Detector
|
17
|
+
# HASH { <id> => Detector }
|
18
|
+
@@instances = {}
|
19
|
+
@@ids = []
|
20
|
+
|
21
|
+
attr_reader :id, :name, :labels, :label_ids, :permission_level, :owner, :training,
|
22
|
+
:type, :state
|
23
|
+
|
24
|
+
# Looks up the detector by matching fields.
|
25
|
+
# :label and :name get matched according the the regular expression /\b(<word>)/i
|
26
|
+
# @example
|
27
|
+
# Matroid::Detector.find(label: 'cat', state: 'trained')
|
28
|
+
# @example
|
29
|
+
# Matroid::Detector.find(name: 'cat')
|
30
|
+
# @example
|
31
|
+
# Matroid::Detector.find(name: 'cat', published: true)
|
32
|
+
# @example
|
33
|
+
# Matroid::Detector.find(label: 'puppy').first.id
|
34
|
+
# @note The detector must either be created by the current authenticated user or published for general use.
|
35
|
+
# @return [Array<Hash><Detector>] Returns the detector instances that match the query.
|
36
|
+
def self.find(args)
|
37
|
+
raise Error::InvalidQueryError.new('Argument must be a hash.') unless args.class == Hash
|
38
|
+
query = args.keys.map{|key| key.to_s + '=' + args[key].to_s }.join('&')
|
39
|
+
detectors = Matroid.get('detectors/search?' + query)
|
40
|
+
detectors.map{|params| register(params) }
|
41
|
+
end
|
42
|
+
|
43
|
+
# Chooses first occurence of the results from {#find}
|
44
|
+
def self.find_one(args)
|
45
|
+
args['limit'] = 1
|
46
|
+
find(args).first
|
47
|
+
end
|
48
|
+
|
49
|
+
# Finds a single document based on the id
|
50
|
+
def self.find_by_id(id, args = {})
|
51
|
+
detector = @@instances[id]
|
52
|
+
is_trained = detector.class == Detector && detector.is_trained?
|
53
|
+
return detector if is_trained
|
54
|
+
|
55
|
+
args[:id] = id
|
56
|
+
find_one(args)
|
57
|
+
end
|
58
|
+
|
59
|
+
SEARCH_PARAMETERS.each do |param|
|
60
|
+
# Search for detectors using the "find_one_by_" method prefix
|
61
|
+
define_singleton_method "find_one_by_#{param}" do |arg, opts = {}|
|
62
|
+
opts[param.to_sym] = arg
|
63
|
+
find_one(opts)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Search for detectors using the "find_by_" method prefix
|
67
|
+
define_singleton_method "find_by_#{param}" do |arg, opts = {}|
|
68
|
+
opts[param.to_sym] = arg
|
69
|
+
find(opts)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# List of cached detectors that have been returned by search requests as hashes or Detector instances.
|
74
|
+
# @param type [String] Indicate how you want the response
|
75
|
+
# @return [Array<Hash, Detector>]
|
76
|
+
def self.cached(type = 'instance')
|
77
|
+
case type
|
78
|
+
when 'hash'
|
79
|
+
@@ids.map{|id| find_by_id(id).to_hash }
|
80
|
+
when 'instance'
|
81
|
+
@@ids.map{|id| find_by_id(id) }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Removes all cached detector instances
|
86
|
+
def self.reset
|
87
|
+
@@instances = {}
|
88
|
+
@@ids = []
|
89
|
+
end
|
90
|
+
|
91
|
+
# Creates a new detector with the contents of a zip file.
|
92
|
+
# The root folder should contain only directories which will become the labels for detection.
|
93
|
+
# Each of these directories should contain only a images corresponding to that label.
|
94
|
+
# Zip file structure example:
|
95
|
+
# cat/
|
96
|
+
# garfield.jpg
|
97
|
+
# nermal.png
|
98
|
+
# dog/
|
99
|
+
# odie.tiff
|
100
|
+
# @note Max 1 GB zip file upload.
|
101
|
+
# @param zip_file [String] Path to zip file containing the images to be used in the detector creation
|
102
|
+
# @param name [String] The detector's display name
|
103
|
+
# @param detector_type [String] Options: "general", "face_detector", or "facial_characteristics"
|
104
|
+
# @return [Detector]
|
105
|
+
def self.create(zip_file, name, detector_type='general')
|
106
|
+
case zip_file
|
107
|
+
when String
|
108
|
+
file = File.new(zip_file, 'rb')
|
109
|
+
when File
|
110
|
+
file = zip_file
|
111
|
+
else
|
112
|
+
err_msg = 'First argument must be a zip file of the image folders, or a string of the path to the file'
|
113
|
+
raise Error::InvalidQueryError.new(err_msg)
|
114
|
+
end
|
115
|
+
params = {
|
116
|
+
file: file,
|
117
|
+
name: name,
|
118
|
+
detector_type: detector_type
|
119
|
+
}
|
120
|
+
response = Matroid.post('detectors', params)
|
121
|
+
id = response['detector_id']
|
122
|
+
find_by_id(id)
|
123
|
+
end
|
124
|
+
|
125
|
+
def initialize(params)
|
126
|
+
update_params(params)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Detector attributes in a nicely printed format for viewing
|
130
|
+
def info
|
131
|
+
puts JSON.pretty_generate(to_hash)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Detector attributes as a hash
|
135
|
+
# @return [Hash]
|
136
|
+
def to_hash
|
137
|
+
instance_variables.each_with_object(Hash.new(0)) do |element, hash|
|
138
|
+
hash["#{element}".delete("@").to_sym] = instance_variable_get(element)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Submits detector instance for training
|
143
|
+
# @note
|
144
|
+
# Fails if detector is not qualified to be trained.
|
145
|
+
def train
|
146
|
+
raise Error::APIError.new("This detector is already trained.") if is_trained?
|
147
|
+
response = Matroid.post("detectors/#{@id}/finalize")
|
148
|
+
response['detector']
|
149
|
+
end
|
150
|
+
|
151
|
+
# @return [Boolean]
|
152
|
+
def is_trained?
|
153
|
+
@state == 'trained'
|
154
|
+
end
|
155
|
+
|
156
|
+
# Updates the the detector data. Used when training to see the detector training progress.
|
157
|
+
# @return [Detector]
|
158
|
+
def update
|
159
|
+
self.class.find_by_id(@id)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Submits an image file via url to be classified with the detector
|
163
|
+
# @param url [String] Url for image file
|
164
|
+
# @return Hash containing the classification data.
|
165
|
+
# @example
|
166
|
+
# det = Matroid::Detector.find_by_id "5893f98530c1c00d0063835b"
|
167
|
+
# det.classify_image_url "https://www.allaboutbirds.org/guide/PHOTO/LARGE/common_tern_donnalynn.jpg"
|
168
|
+
# ### returns hash of results ###
|
169
|
+
# # {
|
170
|
+
# # "results": [
|
171
|
+
# # {
|
172
|
+
# # "file": {
|
173
|
+
# # "name": "image1.png",
|
174
|
+
# # "url": "https://myimages.1.png",
|
175
|
+
# # "thumbUrl": "https://myimages.1_t.png",
|
176
|
+
# # "filetype": "image/png"
|
177
|
+
# # },
|
178
|
+
# # "predictions": [
|
179
|
+
# # {
|
180
|
+
# # "bbox": {
|
181
|
+
# # "left": 0.7533333333333333,
|
182
|
+
# # "top": 0.4504347826086956,
|
183
|
+
# # "height": 0.21565217391304348,
|
184
|
+
# # "aspectRatio": 1.0434782608695652
|
185
|
+
# # },
|
186
|
+
# # "labels": {
|
187
|
+
# # "cat face": 0.7078468322753906,
|
188
|
+
# # "dog face": 0.29215322732925415
|
189
|
+
# # }
|
190
|
+
# # },
|
191
|
+
# # {
|
192
|
+
# # "bbox": {
|
193
|
+
# # "left": 0.4533333333333333,
|
194
|
+
# # "top": 0.6417391304347826,
|
195
|
+
# # "width": 0.20833333333333334,
|
196
|
+
# # "height": 0.21739130434782608,
|
197
|
+
# # "aspectRatio": 1.0434782608695652
|
198
|
+
# # },
|
199
|
+
# # "labels": {
|
200
|
+
# # "cat face": 0.75759859402753906,
|
201
|
+
# # "dog face": 0.45895322732925415
|
202
|
+
# # }
|
203
|
+
# # }, {
|
204
|
+
# # ...
|
205
|
+
# # }
|
206
|
+
# # ]
|
207
|
+
# # }
|
208
|
+
# # ]
|
209
|
+
# # }
|
210
|
+
def classify_image_url(url)
|
211
|
+
classify('image', url: url)
|
212
|
+
end
|
213
|
+
|
214
|
+
# Submits an image file via url to be classified with the detector
|
215
|
+
# @param url [String] Url for image file
|
216
|
+
# @return Hash containing the classification data see {#classify_image_url }
|
217
|
+
# @example
|
218
|
+
# det = Matroid::Detector.find_by_id "5893f98530c1c00d0063835b"
|
219
|
+
# det.classify_image_file "path/to/file.jpg"
|
220
|
+
def classify_image_file(file_path)
|
221
|
+
size_err = "Individual file size must be under #{IMAGE_FILE_SIZE_LIMIT / 1024 / 1024}MB"
|
222
|
+
raise Error::InvalidQueryError.new(size_err) if File.size(file_path) > IMAGE_FILE_SIZE_LIMIT
|
223
|
+
classify('image', file: File.new(file_path, 'rb'))
|
224
|
+
end
|
225
|
+
|
226
|
+
# The plural of {#classify_image_file}
|
227
|
+
# @param file_paths [Array<String>] An array of images in the form of paths from the current directory
|
228
|
+
# @return Hash containing the classification data
|
229
|
+
def classify_image_files(file_paths)
|
230
|
+
arg_err = "Error: Argument must be an array of image file paths"
|
231
|
+
size_err = "Error: Total batch size must be under #{BATCH_FILE_SIZE_LIMIT / 1024 / 1024}MB"
|
232
|
+
raise arg_err unless file_paths.is_a?(Array)
|
233
|
+
batch_size = file_paths.inject(0){ |sum, file| sum + File.size(file) }
|
234
|
+
raise size_err unless batch_size < BATCH_FILE_SIZE_LIMIT
|
235
|
+
files = file_paths.map{ |file_path| ['file', File.new(file_path, 'rb')] }
|
236
|
+
|
237
|
+
url = "#{Matroid.base_api_uri}detectors/#{@id}/classify_image"
|
238
|
+
|
239
|
+
client = HTTPClient.new
|
240
|
+
response = client.post(url, body: files, header: {'Authorization' => Matroid.token.authorization_header})
|
241
|
+
Matroid.parse_response(response)
|
242
|
+
# Matroid.post("detectors/#{@id}/classify_image", files) # responds with 'request entity too large' for some reason
|
243
|
+
end
|
244
|
+
|
245
|
+
# Submits a video file via url to be classified with the detector
|
246
|
+
# @param url [String] Url for video file
|
247
|
+
# @return Hash containing the registered video's id; ex: { "video_id" => "58489472ff22bb2d3f95728c" }. Needed for Matroid.get_video_results(video_id)
|
248
|
+
def classify_video_url(url)
|
249
|
+
classify('video', url: url)
|
250
|
+
end
|
251
|
+
|
252
|
+
# Submits a local video file to be classified with the detector
|
253
|
+
# @param file_path [String] Path to file
|
254
|
+
# @return Hash containing the registered video's id ex: { "video_id" => "58489472ff22bb2d3f95728c" }. Needed for Matroid.get_video_results(video_id)
|
255
|
+
def classify_video_file(file_path)
|
256
|
+
size_err = "Video file size must be under #{VIDEO_FILE_SIZE_LIMIT / 1024 / 1024}MB"
|
257
|
+
raise Error::InvalidQueryError.new(size_err) if File.size(file_path) > VIDEO_FILE_SIZE_LIMIT
|
258
|
+
classify('video', file: File.new(file_path, 'rb'))
|
259
|
+
end
|
260
|
+
|
261
|
+
def update_params(params)
|
262
|
+
@id = params['id'] if params['id']
|
263
|
+
@name = params['name'] if params['name']
|
264
|
+
@labels = params['labels'] if params['labels']
|
265
|
+
@label_ids = params['label_ids'] if params['label_ids']
|
266
|
+
@permission_level = params['permission_level'] if params['permission_level']
|
267
|
+
@owner = params['owner'] if params['owner']
|
268
|
+
@type = params['type'] if params['type']
|
269
|
+
@training = params['training'] if params['training']
|
270
|
+
@state = params['state'] if params['state']
|
271
|
+
self
|
272
|
+
end
|
273
|
+
|
274
|
+
private
|
275
|
+
|
276
|
+
def classify(type, params)
|
277
|
+
not_trained_err = "This detector's training is not complete."
|
278
|
+
raise Error::InvalidQueryError.new(not_trained_err) unless is_trained?
|
279
|
+
Matroid.post("detectors/#{@id}/classify_#{type}", params)
|
280
|
+
end
|
281
|
+
|
282
|
+
def self.register(obj)
|
283
|
+
id = obj['id']
|
284
|
+
@@ids.push(id) if @@instances[id].nil?
|
285
|
+
if @@instances[id].class == Detector
|
286
|
+
@@instances[id].update_params(obj)
|
287
|
+
else
|
288
|
+
@@instances[id] = Detector.new(obj)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
end
|
293
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Matroid
|
2
|
+
module Error
|
3
|
+
class APIError < StandardError; end
|
4
|
+
class APIConnectionError < APIError; end
|
5
|
+
class AuthorizationError < APIError; end
|
6
|
+
class InvalidQueryError < APIError; end
|
7
|
+
class ServerError < APIError; end
|
8
|
+
class RateLimitError < APIError; end
|
9
|
+
class PaymentError < APIError; end
|
10
|
+
class MediaError < APIError; end
|
11
|
+
end
|
12
|
+
end
|
data/lib/matroid/version.rb
CHANGED
data/lib/matroid.rb
CHANGED
@@ -1,43 +1,82 @@
|
|
1
|
-
require
|
2
|
-
require '
|
3
|
-
require '
|
1
|
+
require 'dotenv/load'
|
2
|
+
require 'matroid/connection'
|
3
|
+
require 'matroid/version'
|
4
|
+
require 'matroid/detector'
|
5
|
+
require 'matroid/error'
|
4
6
|
|
5
7
|
module Matroid
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
8
|
+
@client_id = ENV['MATROID_CLIENT_ID']
|
9
|
+
@client_secret = ENV['MATROID_CLIENT_SECRET']
|
10
|
+
|
11
|
+
class << self
|
12
|
+
|
13
|
+
# Authenticates access for Matroid API
|
14
|
+
# @example
|
15
|
+
# Matroid.authenticate("<your_client_id>", "<your_client_secret>")
|
16
|
+
# @param client_id [String]
|
17
|
+
# @param client_secret [String]
|
18
|
+
# @return [Boolean] If the the access token is successfully created.
|
19
|
+
def authenticate(client_id = nil, client_secret = nil)
|
20
|
+
return true unless @token.nil? || @token.expired?
|
21
|
+
if client_id && client_secret
|
22
|
+
err_msg = 'problem using environment variables "MATROID_CLIENT_ID" and "MATROID_CLIENT_SECRET"'
|
23
|
+
new_token = get_token(client_id, client_secret)
|
24
|
+
raise Error::AuthorizationError.new(err_msg) if new_token.nil?
|
25
|
+
@client_id, @client_secret = client_id, client_secret
|
26
|
+
elsif (@client_id.nil? || @client_secret.nil?) && !environment_variables?
|
27
|
+
err_msg = '"MATROID_CLIENT_ID" and "MATROID_CLIENT_SECRET" not found in environment'
|
28
|
+
raise Error::AuthorizationError.new(err_msg)
|
29
|
+
else
|
30
|
+
err_msg = 'Problem using client variables provided'
|
31
|
+
raise Error::AuthorizationError.new(err_msg) if get_token(@client_id, @client_secret).nil?
|
32
|
+
end
|
33
|
+
|
34
|
+
true
|
17
35
|
end
|
18
36
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
client_secret: @client_secret,
|
25
|
-
grant_type: 'client_credentials'
|
26
|
-
}
|
27
|
-
}
|
28
|
-
response = @request.send(endpoint[:method], endpoint[:uri], opts)
|
29
|
-
@authorization_header = "#{response['token_type']} #{response['access_token']}"
|
30
|
-
response.parsed_response
|
37
|
+
# Calls ::show on the current token (if it exists).
|
38
|
+
def show_token
|
39
|
+
if @token
|
40
|
+
@token.show
|
41
|
+
end
|
31
42
|
end
|
32
43
|
|
44
|
+
# Retrieves the authenticated user's account information
|
45
|
+
# @return The account info as a parsed JSON
|
46
|
+
# @example
|
47
|
+
# {
|
48
|
+
# "account" => {
|
49
|
+
# "credits" => {
|
50
|
+
# "concurrentTrainLimit" =>1,
|
51
|
+
# "held" => 3496,
|
52
|
+
# "plan" => "premium",
|
53
|
+
# "daily" => {
|
54
|
+
# "used" => 36842,
|
55
|
+
# "available" => 100000
|
56
|
+
# },
|
57
|
+
# "monthly" => {
|
58
|
+
# "used" => 36842,
|
59
|
+
# "available" => 1000000
|
60
|
+
# }
|
61
|
+
# }
|
62
|
+
# }
|
63
|
+
# }
|
33
64
|
def account_info
|
34
|
-
|
35
|
-
headers = { "Authorization" => @authorization_header }
|
36
|
-
opts = {
|
37
|
-
headers: headers
|
38
|
-
}
|
39
|
-
response = @request.send(endpoint[:method], endpoint[:uri], opts)
|
40
|
-
response.parsed_response
|
65
|
+
get('account')
|
41
66
|
end
|
67
|
+
|
68
|
+
# Retrieves video classification data. Requires a video_id from
|
69
|
+
# {Detector#classify_video_file} or {Detector#classify_video_url}
|
70
|
+
# format 'json'/'csv'
|
71
|
+
# @note A "video_id" is needed to get the classification results
|
72
|
+
# @example
|
73
|
+
# <Detector >.get_video_results(video_id: "23498503uf0dd09", threshold: 30, format: 'json')
|
74
|
+
# @param video_id [String]
|
75
|
+
# @param threshold [Numeric, nil]
|
76
|
+
# @param
|
77
|
+
def get_video_results(video_id, *args)
|
78
|
+
get("videos/#{video_id}", *args)
|
79
|
+
end
|
80
|
+
|
42
81
|
end
|
43
82
|
end
|
data/matroid.gemspec
CHANGED
@@ -30,7 +30,18 @@ Gem::Specification.new do |spec|
|
|
30
30
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
31
31
|
spec.require_paths = ["lib"]
|
32
32
|
|
33
|
+
spec.add_dependency 'omniauth-oauth2', '~> 1.3.1'
|
34
|
+
spec.add_dependency 'httpclient'
|
35
|
+
|
33
36
|
spec.add_development_dependency "bundler", "~> 1.14"
|
34
37
|
spec.add_development_dependency "rake", "~> 10.0"
|
35
|
-
spec.
|
38
|
+
spec.add_development_dependency "minitest"
|
39
|
+
spec.add_development_dependency "vcr"
|
40
|
+
spec.add_development_dependency "fakeweb"
|
41
|
+
spec.add_development_dependency "webmock"
|
42
|
+
spec.add_development_dependency "rspec"
|
43
|
+
|
44
|
+
spec.add_dependency "dotenv"
|
45
|
+
spec.add_dependency "faraday"
|
46
|
+
spec.add_dependency "json"
|
36
47
|
end
|
metadata
CHANGED
@@ -1,15 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: matroid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matroid
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-04-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: omniauth-oauth2
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.3.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.3.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: httpclient
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
13
41
|
- !ruby/object:Gem::Dependency
|
14
42
|
name: bundler
|
15
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -39,19 +67,117 @@ dependencies:
|
|
39
67
|
- !ruby/object:Gem::Version
|
40
68
|
version: '10.0'
|
41
69
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
70
|
+
name: minitest
|
43
71
|
requirement: !ruby/object:Gem::Requirement
|
44
72
|
requirements:
|
45
|
-
- - "
|
73
|
+
- - ">="
|
46
74
|
- !ruby/object:Gem::Version
|
47
|
-
version: 0
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: vcr
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: fakeweb
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: webmock
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rspec
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: dotenv
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
48
146
|
type: :runtime
|
49
147
|
prerelease: false
|
50
148
|
version_requirements: !ruby/object:Gem::Requirement
|
51
149
|
requirements:
|
52
|
-
- - "
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: faraday
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :runtime
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: json
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
type: :runtime
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
53
179
|
- !ruby/object:Gem::Version
|
54
|
-
version: 0
|
180
|
+
version: '0'
|
55
181
|
description: Check your account status on Matroid. More features coming soon.
|
56
182
|
email:
|
57
183
|
- solutions@matroid.com
|
@@ -67,6 +193,9 @@ files:
|
|
67
193
|
- bin/console
|
68
194
|
- bin/setup
|
69
195
|
- lib/matroid.rb
|
196
|
+
- lib/matroid/connection.rb
|
197
|
+
- lib/matroid/detector.rb
|
198
|
+
- lib/matroid/error.rb
|
70
199
|
- lib/matroid/version.rb
|
71
200
|
- matroid.gemspec
|
72
201
|
homepage: http://www.matroid.com
|
@@ -90,7 +219,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
90
219
|
version: '0'
|
91
220
|
requirements: []
|
92
221
|
rubyforge_project:
|
93
|
-
rubygems_version: 2.
|
222
|
+
rubygems_version: 2.6.8
|
94
223
|
signing_key:
|
95
224
|
specification_version: 4
|
96
225
|
summary: Matroid API Ruby Library
|