matroid 0.0.1 → 0.0.3
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.
- 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
|