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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +10 -0
  3. data/.gitignore +9 -0
  4. data/.rspec +2 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +81 -0
  8. data/Rakefile +6 -0
  9. data/bin/console +6 -0
  10. data/bin/setup +8 -0
  11. data/circle.yml +3 -0
  12. data/lib/configuration.rb +11 -0
  13. data/lib/google/apis/vision_v1.rb +37 -0
  14. data/lib/google/apis/vision_v1/classes.rb +1259 -0
  15. data/lib/google/apis/vision_v1/representations.rb +370 -0
  16. data/lib/google/apis/vision_v1/service.rb +91 -0
  17. data/lib/guard_validator.rb +58 -0
  18. data/lib/picguard.rb +40 -0
  19. data/lib/picguard/version.rb +3 -0
  20. data/lib/services/analyzer.rb +40 -0
  21. data/lib/services/builders/annotate_image.rb +16 -0
  22. data/lib/services/builders/batch.rb +15 -0
  23. data/lib/services/builders/feature.rb +16 -0
  24. data/lib/services/builders/image.rb +15 -0
  25. data/lib/services/builders/request.rb +27 -0
  26. data/lib/services/builders/vision.rb +17 -0
  27. data/lib/services/image_preparator.rb +46 -0
  28. data/lib/services/validators/likelihood.rb +31 -0
  29. data/picguard.gemspec +30 -0
  30. data/spec/guard_validator_spec.rb +72 -0
  31. data/spec/picguard_spec.rb +22 -0
  32. data/spec/services/analyzer_spec.rb +33 -0
  33. data/spec/services/builders/annotate_image_spec.rb +15 -0
  34. data/spec/services/builders/batch_spec.rb +18 -0
  35. data/spec/services/builders/feature_spec.rb +26 -0
  36. data/spec/services/builders/image_spec.rb +12 -0
  37. data/spec/services/builders/vision_spec.rb +14 -0
  38. data/spec/services/image_preparator_spec.rb +36 -0
  39. data/spec/services/validators/likelihood_spec.rb +26 -0
  40. data/spec/spec_helper.rb +10 -0
  41. data/spec/support/img/cat.jpg +0 -0
  42. data/spec/support/img/face-example.jpg +0 -0
  43. data/spec/support/img/gun-violence.jpg +0 -0
  44. data/spec/support/result_hash_stub.rb +212 -0
  45. 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
@@ -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,3 @@
1
+ module Picguard
2
+ VERSION = "1.0.1"
3
+ 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
@@ -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