picguard 1.0.0 → 1.0.1
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/.codeclimate.yml +10 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +81 -0
- data/Rakefile +6 -0
- data/bin/console +6 -0
- data/bin/setup +8 -0
- data/circle.yml +3 -0
- data/lib/configuration.rb +11 -0
- data/lib/google/apis/vision_v1.rb +37 -0
- data/lib/google/apis/vision_v1/classes.rb +1259 -0
- data/lib/google/apis/vision_v1/representations.rb +370 -0
- data/lib/google/apis/vision_v1/service.rb +91 -0
- data/lib/guard_validator.rb +58 -0
- data/lib/picguard.rb +40 -0
- data/lib/picguard/version.rb +3 -0
- data/lib/services/analyzer.rb +40 -0
- data/lib/services/builders/annotate_image.rb +16 -0
- data/lib/services/builders/batch.rb +15 -0
- data/lib/services/builders/feature.rb +16 -0
- data/lib/services/builders/image.rb +15 -0
- data/lib/services/builders/request.rb +27 -0
- data/lib/services/builders/vision.rb +17 -0
- data/lib/services/image_preparator.rb +46 -0
- data/lib/services/validators/likelihood.rb +31 -0
- data/picguard.gemspec +30 -0
- data/spec/guard_validator_spec.rb +72 -0
- data/spec/picguard_spec.rb +22 -0
- data/spec/services/analyzer_spec.rb +33 -0
- data/spec/services/builders/annotate_image_spec.rb +15 -0
- data/spec/services/builders/batch_spec.rb +18 -0
- data/spec/services/builders/feature_spec.rb +26 -0
- data/spec/services/builders/image_spec.rb +12 -0
- data/spec/services/builders/vision_spec.rb +14 -0
- data/spec/services/image_preparator_spec.rb +36 -0
- data/spec/services/validators/likelihood_spec.rb +26 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/support/img/cat.jpg +0 -0
- data/spec/support/img/face-example.jpg +0 -0
- data/spec/support/img/gun-violence.jpg +0 -0
- data/spec/support/result_hash_stub.rb +212 -0
- metadata +64 -4
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'picguard'
|
2
|
+
require 'active_model'
|
3
|
+
|
4
|
+
class GuardValidator < ActiveModel::EachValidator
|
5
|
+
|
6
|
+
def validate_each(record, attribute, value)
|
7
|
+
image_path = fetch_image_path(record, attribute)
|
8
|
+
return if valid?(image_path)
|
9
|
+
record.errors.add(attribute, @message, options.merge(value: value))
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def fetch_image_path(record, attribute)
|
15
|
+
arr = [attribute].push(*Array(options[:method_name]))
|
16
|
+
arr.inject(record, :public_send)
|
17
|
+
end
|
18
|
+
|
19
|
+
def valid?(image_path)
|
20
|
+
return false unless path_exists?(image_path)
|
21
|
+
result = Picguard.analyze(
|
22
|
+
image_path:
|
23
|
+
image_path,
|
24
|
+
safe_search:
|
25
|
+
options[:safe_search] || false,
|
26
|
+
face_detection:
|
27
|
+
options[:face_detection] || false,
|
28
|
+
threshold_adult:
|
29
|
+
options[:threshold_adult] || Picguard.configuration.threshold_adult,
|
30
|
+
threshold_violence:
|
31
|
+
options[:threshold_violence] || Picguard.configuration.threshold_violence,
|
32
|
+
threshold_face:
|
33
|
+
options[:threshold_face] || Picguard.configuration.threshold_face,
|
34
|
+
)
|
35
|
+
|
36
|
+
return false if options[:safe_search] && safety_violated?(result)
|
37
|
+
return false if options[:face_detection] && !face_recognised?(result)
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
41
|
+
def safety_violated?(result)
|
42
|
+
return false if (!result[:safe_search][:adult] && !result[:safe_search][:violence])
|
43
|
+
@message = 'Picture shows inappropriate content.'
|
44
|
+
true
|
45
|
+
end
|
46
|
+
|
47
|
+
def path_exists?(image_path)
|
48
|
+
return true if image_path && File.exist?(image_path)
|
49
|
+
@message = 'Picture doesn\'t exist.'
|
50
|
+
false
|
51
|
+
end
|
52
|
+
|
53
|
+
def face_recognised?(result)
|
54
|
+
return true if options[:face_detection] && result[:face_recognised]
|
55
|
+
@message = 'Face could not be recognised on given picture.'
|
56
|
+
false
|
57
|
+
end
|
58
|
+
end
|
data/lib/picguard.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require "picguard/version"
|
2
|
+
require "json"
|
3
|
+
# require "picguard_init"
|
4
|
+
require "guard_validator"
|
5
|
+
require "configuration"
|
6
|
+
require "services/builders/request"
|
7
|
+
require "services/analyzer"
|
8
|
+
require "services/image_preparator"
|
9
|
+
|
10
|
+
|
11
|
+
module Picguard
|
12
|
+
class << self
|
13
|
+
attr_accessor :configuration
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.configure
|
17
|
+
self.configuration ||= Configuration.new
|
18
|
+
yield(configuration)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.analyze(image_path:,
|
22
|
+
safe_search: true,
|
23
|
+
face_detection: true,
|
24
|
+
threshold_adult: Picguard.configuration.threshold_adult,
|
25
|
+
threshold_violence: Picguard.configuration.threshold_violence,
|
26
|
+
threshold_face: Picguard.configuration.threshold_face
|
27
|
+
)
|
28
|
+
|
29
|
+
prepared_image_path = Services::ImagePreparator.new(image_path, face_detection, safe_search).call
|
30
|
+
|
31
|
+
Services::Analyzer.new(
|
32
|
+
Services::Builders::Request.new(
|
33
|
+
prepared_image_path, safe_search, face_detection
|
34
|
+
).call,
|
35
|
+
threshold_adult,
|
36
|
+
threshold_violence,
|
37
|
+
threshold_face
|
38
|
+
).call
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'services/validators/likelihood'
|
2
|
+
|
3
|
+
module Services
|
4
|
+
class Analyzer
|
5
|
+
def initialize(result, threshold_adult, threshold_violence, threshold_face)
|
6
|
+
@result = result
|
7
|
+
@threshold_adult = threshold_adult
|
8
|
+
@threshold_violence = threshold_violence
|
9
|
+
@threshold_face = threshold_face
|
10
|
+
end
|
11
|
+
|
12
|
+
def call
|
13
|
+
result_hash = {}
|
14
|
+
unless @result[:safe_search_annotation].nil?
|
15
|
+
result_hash[:safe_search] = analyze_safe_search_annotation(@result[:safe_search_annotation])
|
16
|
+
end
|
17
|
+
|
18
|
+
unless @result[:face_annotations].nil?
|
19
|
+
detection_confidence = @result[:face_annotations].first[:detection_confidence]
|
20
|
+
end
|
21
|
+
detection_confidence ||= 0
|
22
|
+
|
23
|
+
result_hash[:face_recognised] = analyze_face_annotation(detection_confidence)
|
24
|
+
result_hash
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def analyze_safe_search_annotation(hash_result)
|
30
|
+
{
|
31
|
+
violence: Validators::Likelihood.new(hash_result[:violence], @threshold_violence).call,
|
32
|
+
adult: Validators::Likelihood.new(hash_result[:adult], @threshold_adult).call,
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def analyze_face_annotation(float_result)
|
37
|
+
float_result > @threshold_face
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require "google/apis/vision_v1"
|
2
|
+
|
3
|
+
module Services
|
4
|
+
module Builders
|
5
|
+
class AnnotateImage
|
6
|
+
def initialize(image, features)
|
7
|
+
@image = image
|
8
|
+
@features = features
|
9
|
+
end
|
10
|
+
|
11
|
+
def call
|
12
|
+
Google::Apis::VisionV1::AnnotateImageRequest.new(image: @image, features: @features)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "google/apis/vision_v1"
|
2
|
+
|
3
|
+
module Services
|
4
|
+
module Builders
|
5
|
+
class BatchAnnotateImages
|
6
|
+
def initialize(image_requests)
|
7
|
+
@image_requests = image_requests
|
8
|
+
end
|
9
|
+
|
10
|
+
def call
|
11
|
+
Google::Apis::VisionV1::BatchAnnotateImagesRequest.new(requests: @image_requests)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require "google/apis/vision_v1"
|
2
|
+
|
3
|
+
module Services
|
4
|
+
module Builders
|
5
|
+
class Feature
|
6
|
+
def initialize(type, max_results = '1')
|
7
|
+
@type = type
|
8
|
+
@max_results = max_results
|
9
|
+
end
|
10
|
+
|
11
|
+
def call
|
12
|
+
Google::Apis::VisionV1::Feature.new(max_results: @max_results, type: @type)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "google/apis/vision_v1"
|
2
|
+
|
3
|
+
module Services
|
4
|
+
module Builders
|
5
|
+
class Image
|
6
|
+
def initialize(image_path)
|
7
|
+
@image_path = image_path
|
8
|
+
end
|
9
|
+
|
10
|
+
def call
|
11
|
+
Google::Apis::VisionV1::Image.new(content: open(@image_path).to_a.join)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "google/apis/vision_v1"
|
2
|
+
%w[annotate_image batch feature image vision].each do |serv|
|
3
|
+
require "services/builders/#{serv}"
|
4
|
+
end
|
5
|
+
|
6
|
+
module Services
|
7
|
+
module Builders
|
8
|
+
class Request
|
9
|
+
def initialize(image_path, safe_search, face_detection)
|
10
|
+
@image_path = image_path
|
11
|
+
@safe_search = safe_search
|
12
|
+
@face_detection = face_detection
|
13
|
+
@features = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def call
|
17
|
+
vision = Services::Builders::Vision.new.call
|
18
|
+
@features << Services::Builders::Feature.new("SAFE_SEARCH_DETECTION").call if @safe_search
|
19
|
+
@features << Services::Builders::Feature.new("FACE_DETECTION").call if @face_detection
|
20
|
+
image = Services::Builders::Image.new(@image_path).call
|
21
|
+
annotate_image = Services::Builders::AnnotateImage.new(image, @features).call
|
22
|
+
batch_annotate = Services::Builders::BatchAnnotateImages.new([annotate_image]).call
|
23
|
+
vision.annotate_image(batch_annotate).to_h[:responses].first
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "google/apis/vision_v1"
|
2
|
+
|
3
|
+
module Services
|
4
|
+
module Builders
|
5
|
+
class Vision
|
6
|
+
def initialize
|
7
|
+
@key = Picguard.configuration.google_api_key
|
8
|
+
end
|
9
|
+
|
10
|
+
def call
|
11
|
+
vision = Google::Apis::VisionV1::VisionService.new
|
12
|
+
vision.key = @key
|
13
|
+
vision
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "mini_magick"
|
2
|
+
|
3
|
+
module Services
|
4
|
+
class ImagePreparator
|
5
|
+
def initialize(image_path, face_detection, safe_search)
|
6
|
+
@image_path = image_path
|
7
|
+
@face_detection = face_detection
|
8
|
+
@safe_search = safe_search
|
9
|
+
end
|
10
|
+
|
11
|
+
RECOMMENDED_SIZES = {
|
12
|
+
face_detection: %w(1600x1200 1200x1600),
|
13
|
+
safe_search: %w(640x480 480x640)
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
private_constant :RECOMMENDED_SIZES
|
17
|
+
|
18
|
+
def call
|
19
|
+
image = MiniMagick::Image.open(@image_path)
|
20
|
+
return @image_path unless oversized?(image)
|
21
|
+
resize_image(image)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def oversized?(image)
|
27
|
+
dimensions = image.dimensions
|
28
|
+
dimensions.any? { |dimension| dimension > 1600 }
|
29
|
+
end
|
30
|
+
|
31
|
+
def resize_image(image)
|
32
|
+
image.resize(resize_dimensions(image)).path
|
33
|
+
end
|
34
|
+
|
35
|
+
def resize_resolution(filter, order)
|
36
|
+
RECOMMENDED_SIZES.fetch(filter.to_sym)[order]
|
37
|
+
end
|
38
|
+
|
39
|
+
def resize_dimensions(image)
|
40
|
+
width, height = image.dimensions
|
41
|
+
resize_to = @face_detection ? "face_detection" : "safe_search"
|
42
|
+
order = width > height ? 0 : 1
|
43
|
+
resize_resolution(resize_to, order)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'picguard'
|
2
|
+
|
3
|
+
module Services
|
4
|
+
module Validators
|
5
|
+
class Likelihood
|
6
|
+
RESPONSES = {
|
7
|
+
unknown: 0,
|
8
|
+
very_unlikely: 1,
|
9
|
+
unlikely: 2,
|
10
|
+
possible: 3,
|
11
|
+
likely: 4,
|
12
|
+
very_likely: 5,
|
13
|
+
}.freeze
|
14
|
+
private_constant :RESPONSES
|
15
|
+
|
16
|
+
def initialize(response, threshold)
|
17
|
+
@response, @threshold = response, threshold
|
18
|
+
end
|
19
|
+
|
20
|
+
def call
|
21
|
+
value_for(@response) > value_for(@threshold)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def value_for(response)
|
27
|
+
RESPONSES.fetch(response.downcase.to_sym)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/picguard.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'picguard/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "picguard"
|
8
|
+
spec.version = Picguard::VERSION
|
9
|
+
spec.authors = ["Szymon Baranowski", "Tomasz Jaśkiewicz"]
|
10
|
+
spec.email = ["szymon.baranowski@netguru.pl", "tomasz.jaskiewicz@netguru.pl"]
|
11
|
+
|
12
|
+
spec.summary = %q{A gem for filtering a pictures that are being uploaded to your server.}
|
13
|
+
spec.description = %q{Picguard guards your application by filtering out the pictures containing inappropriate content.}
|
14
|
+
spec.homepage = "https://github.com/netguru/picguard"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_dependency "google-api-client"
|
23
|
+
spec.add_dependency "mini_magick"
|
24
|
+
spec.add_dependency "activemodel"
|
25
|
+
spec.add_development_dependency "pry"
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.11"
|
27
|
+
spec.add_development_dependency "codeclimate-test-reporter"
|
28
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
29
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
30
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
class ImageStruct
|
5
|
+
def initialize(path)
|
6
|
+
@path = path
|
7
|
+
end
|
8
|
+
|
9
|
+
def img_path
|
10
|
+
@path
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class ModelStruct
|
15
|
+
include ActiveModel::Validations
|
16
|
+
def initialize(image_struct)
|
17
|
+
@image = image_struct
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_accessor :image
|
21
|
+
|
22
|
+
validates :image, guard: {
|
23
|
+
safe_search: true,
|
24
|
+
face_detection: true,
|
25
|
+
method_name: :img_path }
|
26
|
+
end
|
27
|
+
|
28
|
+
describe GuardValidator do
|
29
|
+
before(:all) do
|
30
|
+
Picguard.configure do |config|
|
31
|
+
config.google_api_key = ENV['GCLOUD_KEY']
|
32
|
+
config.threshold_adult = "POSSIBLE"
|
33
|
+
config.threshold_violence = "UNLIKELY"
|
34
|
+
config.threshold_face = 0.8
|
35
|
+
end
|
36
|
+
end
|
37
|
+
subject { ModelStruct.new(image_struct) }
|
38
|
+
|
39
|
+
context 'with valid picture' do
|
40
|
+
let(:image_struct) { ImageStruct.new("spec/support/img/face-example.jpg") }
|
41
|
+
it 'is valid' do
|
42
|
+
expect(subject).to be_valid
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'face unrecognised' do
|
47
|
+
let(:image_struct) { ImageStruct.new("spec/support/img/cat.jpg") }
|
48
|
+
it 'is invalid' do
|
49
|
+
expect(subject).not_to be_valid
|
50
|
+
expect(subject.errors[:image].size).to eq(1)
|
51
|
+
expect(subject.errors.messages[:image].first).to eq("Face could not be recognised on given picture.")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'violent content' do
|
56
|
+
let(:image_struct) { ImageStruct.new("spec/support/img/gun-violence.jpg") }
|
57
|
+
it 'is invalid' do
|
58
|
+
expect(subject).not_to be_valid
|
59
|
+
expect(subject.errors[:image].size).to eq(1)
|
60
|
+
expect(subject.errors.messages[:image].first).to eq("Picture shows inappropriate content.")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'path to picture invalid' do
|
65
|
+
let(:image_struct) { ImageStruct.new("") }
|
66
|
+
it 'is invalid' do
|
67
|
+
expect(subject).not_to be_valid
|
68
|
+
expect(subject.errors[:image].size).to eq(1)
|
69
|
+
expect(subject.errors.messages[:image].first).to eq ("Picture doesn't exist.")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|