photographic_memory 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/photographic_memory.rb +260 -0
  3. metadata +101 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1a68924057c21aeb4257048b809789a2c9ce0284
4
+ data.tar.gz: da5658dd3a42a345af1f9d76d1735f917b4d59fd
5
+ SHA512:
6
+ metadata.gz: 67b46e485cb4c9c9d491e8bf72e4b9cd5b7b639928248b498a2b514f3e99b974fe0264edbc04153e459286d3cff6a4f6390d2f30e5355e6fbb65b98eb0413f2c
7
+ data.tar.gz: dac138da3170cb38f4dbf0e397452826143d3165d83ca1060c14630c1c484fcc4bf98d3076bf0d1c0d10b338543bdee99d4cfbcb1904d198812c476fb0f95f3d
@@ -0,0 +1,260 @@
1
+ require "timeout"
2
+ require "open3"
3
+ require "digest"
4
+ require "rack/mime"
5
+ require "aws-sdk"
6
+ require "mini_exiftool"
7
+
8
+ ##
9
+ # An image processing client that uses ImageMagick's convert and an AWS S3-like API for storage.
10
+ #
11
+ # @example
12
+ #
13
+ # client = PhotographicMemory.new(@config)
14
+ # client.put file: image, id: 123
15
+ #
16
+ # @param [Hash] config
17
+ # @param [String] config[:environment] - The application environment. Is optional and only changes behavior with the string "test", which stubs S3 responses and prevents calls to Rekognition.
18
+ # @param [String] config[:s3_region] - The region to use for S3. Only relevant when actually using AWS S3.
19
+ # @param [String] config[:s3_endpoint] - The endpoint to use for S3 calls. Only required when using your own S3-compatible storage medium.
20
+ # @param [Boolean] config[:s3_force_path_style] - Forces path style for S3 API calls. Defaults to true.
21
+ # @param [String] config[:s3_access_key_id] - The access key ID for S3 calls.
22
+ # @param [String] config[:s3_secret_access_key] - The secret access key for S3 calls.
23
+ # @param [string] config[:s3_signature_version] - The signature version for S3 calls. Defaults to 's3'.
24
+ # @param [string] config[:rekognition_access_key_id] - The access key ID for Rekognition calls.
25
+ # @param [string] config[:rekognition_secret_access_key] - The secret access key for Rekognition calls.
26
+ # @param [string] config[:rekognition_region] - The region for Rekognition calls.
27
+ #
28
+ class PhotographicMemory
29
+
30
+ attr_accessor :config, :s3_client
31
+
32
+ class PhotographicMemoryError < StandardError; end
33
+
34
+ def initialize config={}
35
+ @config = config
36
+ options = {
37
+ region: config[:s3_region],
38
+ endpoint: config[:s3_endpoint],
39
+ force_path_style: config[:s3_force_path_style] || true,
40
+ credentials: Aws::Credentials.new(
41
+ config[:s3_access_key_id],
42
+ config[:s3_secret_access_key]
43
+ ),
44
+ stub_responses: config[:environment] === "test",
45
+ signature_version: config[:s3_signature_version] || "s3"
46
+ }.select{|k,v| !v.nil?}
47
+ @s3_client = Aws::S3::Client.new(options)
48
+ end
49
+
50
+ def put file:, id:, style_name:"original", convert_options: [], content_type:
51
+ unless (style_name == "original") || convert_options.empty?
52
+ if content_type.match "image/gif"
53
+ output = render_gif file, convert_options
54
+ else
55
+ output = render file, convert_options
56
+ end
57
+ else
58
+ output = file.read
59
+ end
60
+ file.rewind
61
+ original_digest = Digest::MD5.hexdigest(file.read)
62
+ rendered_digest = Digest::MD5.hexdigest(output)
63
+ output_fingerprint = (style_name == "original") ? "original" : rendered_digest
64
+ extension = Rack::Mime::MIME_TYPES.invert[content_type]
65
+ key = "#{id}_#{original_digest}_#{output_fingerprint}#{extension}"
66
+ @s3_client.put_object({
67
+ bucket: @config[:s3_bucket],
68
+ key: key,
69
+ body: output,
70
+ content_type: content_type
71
+ })
72
+ if style_name == "original" && config[:environment] != "test"
73
+ reference = StringIO.new render(file, ["-quality 10"])
74
+ # 👆 this is a low quality reference image we generate
75
+ # which is sufficient for classification purposes but
76
+ # saves bandwidth and overcomes the file size limit
77
+ # for Rekognition
78
+ keywords = detect_labels reference
79
+ gravity = detect_gravity reference
80
+ else
81
+ keywords = []
82
+ gravity = "Center"
83
+ end
84
+ {
85
+ fingerprint: rendered_digest,
86
+ metadata: exif(file),
87
+ extension: extension,
88
+ filename: key,
89
+ keywords: keywords,
90
+ gravity: gravity
91
+ }
92
+ end
93
+
94
+ def get key
95
+ @s3_client.get_object({
96
+ bucket: @config[:s3_bucket],
97
+ key: key
98
+ }).body
99
+ end
100
+
101
+ def delete key
102
+ @s3_client.delete_object({
103
+ bucket: @config[:s3_bucket],
104
+ key: key
105
+ })
106
+ end
107
+
108
+ private
109
+
110
+ def exif file
111
+ file.rewind
112
+ MiniExiftool.new(file, :replace_invalid_chars => "")
113
+ end
114
+
115
+ def render file, convert_options=[]
116
+ file.rewind
117
+ run_command ["convert", "-", convert_options, "jpeg:-"].flatten.join(" "), file.read.force_encoding("UTF-8")
118
+ end
119
+
120
+ def render_gif file, convert_options=[]
121
+ convert_options.concat(["-coalesce", "-repage 0x0", "+repage"])
122
+ convert_options.each do |option|
123
+ if option.match("-crop")
124
+ option.concat " +repage"
125
+ end
126
+ end
127
+ file.rewind
128
+ run_command ["convert", "-", convert_options, "gif:-"].flatten.join(" "), file.read.force_encoding("UTF-8")
129
+ end
130
+
131
+ def classify file
132
+ file.rewind
133
+ detect_labels file
134
+ rescue Aws::Rekognition::Errors::ServiceError, Aws::Errors::MissingRegionError, Seahorse::Client::NetworkingError
135
+ # This also is not worth crashing over
136
+ []
137
+ end
138
+
139
+ def detect_gravity file
140
+ file.rewind
141
+
142
+ boxes = detect_faces(file).map(&:bounding_box)
143
+
144
+ box = boxes.max_by{|b| b.width * b.height } # use the largest face in the photo
145
+
146
+ return "Center" if !box
147
+
148
+ x = nearest_fifth((box.width / 2) + ((box.left >= 0) ? box.left : 0))
149
+ y = nearest_fifth((box.height / 2) + ((box.top >= 0) ? box.top : 0))
150
+
151
+ gravity_table = {
152
+ 0.0 => {
153
+ 0.0 => "NorthWest",
154
+ 0.5 => "West",
155
+ 1.0 => "SouthWest"
156
+ },
157
+ 0.5 => {
158
+ 0.0 => "North",
159
+ 0.5 => "Center",
160
+ 1 => "South"
161
+ },
162
+ 1.0 => {
163
+ 0.0 => "NorthEast",
164
+ 0.5 => "East",
165
+ 1.0 => "SouthEast"
166
+ }
167
+ }
168
+
169
+ gravity_table[x][y]
170
+ end
171
+
172
+ def nearest_fifth num
173
+ (num * 2).round / 2.0
174
+ end
175
+
176
+ def detect_labels file
177
+ file.rewind
178
+ # get the original image from S3 and classify
179
+ client = Aws::Rekognition::Client.new({
180
+ region: @config[:rekognition_region],
181
+ credentials: Aws::Credentials.new(
182
+ @config[:rekognition_access_key_id],
183
+ @config[:rekognition_secret_access_key]
184
+ )
185
+ })
186
+ client.detect_labels({
187
+ image: {
188
+ bytes: file
189
+ },
190
+ max_labels: 123,
191
+ min_confidence: 73,
192
+ }).labels
193
+ rescue Aws::Rekognition::Errors::ServiceError, Aws::Errors::MissingRegionError, Seahorse::Client::NetworkingError => e
194
+ []
195
+ end
196
+
197
+ def detect_faces file
198
+ file.rewind
199
+ # get the original image from S3 and classify
200
+ client = Aws::Rekognition::Client.new({
201
+ region: @config[:rekognition_region],
202
+ credentials: Aws::Credentials.new(
203
+ @config[:rekognition_access_key_id],
204
+ @config[:rekognition_secret_access_key]
205
+ )
206
+ })
207
+ client.detect_faces({
208
+ image: {
209
+ bytes: file
210
+ },
211
+ attributes: ["ALL"]
212
+ }).face_details
213
+ rescue Aws::Rekognition::Errors::ServiceError, Aws::Errors::MissingRegionError, Seahorse::Client::NetworkingError => e
214
+ []
215
+ end
216
+
217
+ def run_command command, input
218
+ stdin, stdout, stderr, wait_thr = Open3.popen3(command)
219
+ pid = wait_thr.pid
220
+
221
+ Timeout.timeout(10) do # cancel in 10 seconds
222
+ stdin.write input
223
+ stdin.close
224
+
225
+ output_buffer = []
226
+ error_buffer = []
227
+
228
+ while (output_chunk = stdout.gets) || (error_chunk = stderr.gets)
229
+ output_buffer << output_chunk
230
+ error_buffer << error_chunk
231
+ end
232
+
233
+ output_buffer.compact!
234
+ error_buffer.compact!
235
+
236
+ output = output_buffer.any? ? output_buffer.join('') : nil
237
+ error = error_buffer.any? ? error_buffer.join('') : nil
238
+
239
+ unless error
240
+ raise PhotographicMemoryError, "No output received." if !output
241
+ else
242
+ raise PhotographicMemoryError, error
243
+ end
244
+ output
245
+ end
246
+ rescue Timeout::Error, Errno::EPIPE => e
247
+ raise PhotographicMemoryError, e.message
248
+ ensure
249
+ begin
250
+ Process.kill("KILL", pid) if pid
251
+ rescue Errno::ESRCH
252
+ # Process is already dead so do nothing.
253
+ end
254
+ stdin = nil
255
+ stdout = nil
256
+ stderr = nil
257
+ wait_thr.value if wait_thr # Process::Status object returned.
258
+ end
259
+ end
260
+
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: photographic_memory
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Ten Bitcomb
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-04-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: mini_exiftool
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.8.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.8.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rack
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 2.0.3
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 2.0.3
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 5.11.3
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 5.11.3
69
+ description: Simple image processing and storage
70
+ email: tenbitcomb@gmail.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - lib/photographic_memory.rb
76
+ homepage: http://github.com/scpr/photographic_memory
77
+ licenses:
78
+ - MIT
79
+ metadata: {}
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 2.5.1
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: Simple image processing and storage
100
+ test_files: []
101
+ has_rdoc: