photographic_memory 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 +7 -0
- data/lib/photographic_memory.rb +260 -0
- metadata +101 -0
checksums.yaml
ADDED
@@ -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:
|