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