dhashy 1.0.0 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/README.md +9 -1
- data/Rakefile +54 -29
- data/dhashy.gemspec +3 -3
- data/lib/dhashy/version.rb +1 -1
- data/lib/dhashy.rb +109 -17
- metadata +9 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6fa31671461b8254fadbaf8c8cf8f8e6aee66236781a2981e9b72295b3a3cf7b
|
4
|
+
data.tar.gz: 867ed25bb583d62cc4ef510b659fe9f51029cba9f430fdab0bf8c66062181dd1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9a4af5da1043c7c8ef48afc9bc4d5c0a2a9017904cde5aab2de3781f93c6a74dbf1c09a44c613c26236fb6de88b48d4bba26df251641ff7be70c046fbb14d160
|
7
|
+
data.tar.gz: fd6773e276b41aadd428429c827c0f99c49434440477ff557af055a219a9cf7409ce8ec47d4754e9d643d3c959d2dd9bc8cf945760d192bab0647f2e7592105e
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
# Dhashy
|
2
2
|
|
3
|
+
Dhashy is a ruby gem implementing the *difference hash* algorithm for perceptual image matching.
|
4
|
+
|
5
|
+
## Dependencies
|
6
|
+
|
7
|
+
Dhashy is tested with ```imagemagick```, version 7. ```Graphicsmagick``` may or may not work.
|
8
|
+
|
9
|
+
You can choose between ```mini_magick``` or ```vips``` for image processing, via the [```image_processing```](https://github.com/janko-m/image_processing) gem. When using ```vips```.
|
10
|
+
|
3
11
|
## Installation
|
4
12
|
|
5
13
|
Add this line to your application's Gemfile:
|
@@ -19,7 +27,7 @@ Or install it yourself as:
|
|
19
27
|
## Usage
|
20
28
|
|
21
29
|
first_image = MiniMagick::Image.open("test/test.jpg")
|
22
|
-
other_image = MiniMagick::Image.open("test/
|
30
|
+
other_image = MiniMagick::Image.open("test/8x8.jpg")
|
23
31
|
first_hash = Dhashy.new(first_image)
|
24
32
|
|
25
33
|
other_hash = Dhashy.new(other_image)
|
data/Rakefile
CHANGED
@@ -1,20 +1,39 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
require "rake/testtask"
|
3
|
-
|
4
3
|
Rake::TestTask.new(:test) do |t|
|
5
4
|
t.libs << "test"
|
6
5
|
t.libs << "lib"
|
7
6
|
t.test_files = FileList["test/**/*_test.rb"]
|
8
7
|
end
|
9
8
|
|
10
|
-
task :
|
9
|
+
task :tree do |t|
|
10
|
+
require '../minimagick/lib/mini_magick'
|
11
|
+
require './lib/dhashy'
|
12
|
+
|
13
|
+
results = []
|
14
|
+
(0..9).each do |folder|
|
15
|
+
ff = "../chill/data/images/004/%03i/???/1.jpg" % folder
|
16
|
+
puts ff
|
17
|
+
Pathname.glob(ff).combination(2).to_a.shuffle.each do |first, second|
|
18
|
+
puts "#{first} #{second}"
|
19
|
+
f1 = Dhashy.new(first)
|
20
|
+
f2 = Dhashy.new(second)
|
21
|
+
diff = f1 - f2
|
22
|
+
results << diff
|
23
|
+
puts "difference %2i avg %2i" % [diff, results.sum / results.size]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
task :trials do |t|
|
11
29
|
|
12
30
|
require "mini_magick"
|
31
|
+
require './lib/dhashy'
|
13
32
|
parameters = {}
|
14
33
|
parameters[:resize] = (0..9).map {|x| 100 - x * 10}
|
15
34
|
parameters[:stretch] = ["100%", "10%", "25%", "50%", "75%", "133%", "200%", "400%", "1000%"]
|
16
35
|
# parameters[:resize_y] = (1..10).map {|x| x * 10}
|
17
|
-
parameters[:crop] = [false, "90%x90%+5%+5%", "50%x50%+25%+25%","90%x10%-1-1", "10%x100%-10%-10%"]
|
36
|
+
#parameters[:crop] = [false, "90%x90%+5%+5%", "50%x50%+25%+25%","90%x10%-1-1", "10%x100%-10%-10%"]
|
18
37
|
parameters[:rotate] = (0..7).map {|x| x * 45}
|
19
38
|
parameters[:flip] = [false, true]
|
20
39
|
parameters[:monochrome] = [false, true]
|
@@ -24,41 +43,47 @@ task :trials do
|
|
24
43
|
parameters.each do |key, values|
|
25
44
|
results[key] = Hash[values.map {|value| [value.to_s, []]}]
|
26
45
|
end
|
27
|
-
|
28
|
-
original = Dhashy.new(MiniMagick::Image.open("test/test.jpg"))
|
46
|
+
original = Dhashy.new(MiniMagick::Image.open("test/test.jpg"), 128)
|
29
47
|
|
30
48
|
parameters.each do |change, choices|
|
31
49
|
choices.each do |choice|
|
32
50
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
51
|
+
settings = Hash[parameters.map {|parameter, values| [parameter, values.first]}]
|
52
|
+
settings[change] = choice
|
53
|
+
other = MiniMagick::Image.open("test/test.jpg")
|
54
|
+
t = other.get_pixels.size
|
55
|
+
other.combine_options do |image|
|
56
|
+
image.resize "#{settings[:resize]}%x#{settings[:stretch]}!"
|
57
|
+
image.rotate settings[:rotate]
|
58
|
+
image.flip if settings[:flip]
|
59
|
+
image.crop settings[:crop] if settings[:crop]
|
60
|
+
settings[:contrast].times { image.contrast } if settings[:contrast]
|
61
|
+
image.enhance if settings[:enhance]
|
62
|
+
image.monochrome if settings[:monochrome]
|
63
|
+
end
|
64
|
+
|
65
|
+
u = other.get_pixels.size
|
66
|
+
otherhash = Dhashy.new(other)
|
67
|
+
puts "original"
|
68
|
+
puts original.display
|
69
|
+
puts "other"
|
70
|
+
puts otherhash.display
|
71
|
+
diff = original - otherhash
|
72
|
+
puts "diff: #{diff}"
|
73
|
+
parameters.each do |key, values|
|
74
|
+
results[key][settings[key].to_s] << diff if key == change
|
75
|
+
end
|
52
76
|
end
|
53
77
|
end
|
54
|
-
|
55
|
-
puts results
|
78
|
+
|
56
79
|
results.each do |modification, options|
|
80
|
+
puts modification
|
57
81
|
options.keys.sort.each do |option|
|
58
|
-
|
59
|
-
next unless
|
60
|
-
puts " %2i %4i %s" % [
|
82
|
+
thisresult = options[option]
|
83
|
+
next unless thisresult.size > 0
|
84
|
+
puts " %2i %4i %s" % [thisresult.sum / thisresult.size, thisresult.size, option]
|
61
85
|
end
|
62
86
|
end
|
63
87
|
end
|
88
|
+
|
64
89
|
task :default => :test
|
data/dhashy.gemspec
CHANGED
@@ -19,8 +19,8 @@ Gem::Specification.new do |spec|
|
|
19
19
|
end
|
20
20
|
|
21
21
|
spec.require_paths = ["lib"]
|
22
|
-
spec.add_dependency "mini_magick", "~> 4
|
23
|
-
spec.add_development_dependency "bundler", "~> 1.16
|
24
|
-
spec.add_development_dependency "rake", "~>
|
22
|
+
spec.add_dependency "mini_magick", "~> 4"
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
24
|
+
spec.add_development_dependency "rake", "~> 12.3"
|
25
25
|
spec.add_development_dependency "minitest", "~> 5.0"
|
26
26
|
end
|
data/lib/dhashy/version.rb
CHANGED
data/lib/dhashy.rb
CHANGED
@@ -1,38 +1,130 @@
|
|
1
1
|
require "dhashy/version"
|
2
2
|
|
3
|
+
# @author Matthias Winkelmann <m@matthi.coffee>
|
4
|
+
# The *difference hash* is a compact representation
|
5
|
+
# of an image (i. e. photo) that works well for fuzzy
|
6
|
+
# finding of pairs of images that are substantially
|
7
|
+
# euqal.
|
8
|
+
#
|
9
|
+
|
3
10
|
class Dhashy
|
4
|
-
|
5
|
-
|
11
|
+
attr_reader :size
|
12
|
+
|
13
|
+
def initialize(jpg, size=128)
|
14
|
+
self.size = size
|
15
|
+
|
16
|
+
return from_array(jpg) if jpg.is_a? Array
|
17
|
+
jpg = MiniMagick::Image.open(jpg.to_s) if jpg.is_a? Pathname
|
6
18
|
jpg.combine_options do |j|
|
7
|
-
j.
|
8
|
-
|
19
|
+
j.resize "#{@dimension + 1}x#{@dimension + 1}!"
|
20
|
+
end
|
21
|
+
@pixels = jpg.get_pixels.map {|row| row.map {|p| ((p[0].to_f * 299) + (p[1] * 587) + (p[2] * 114)).to_f / 1000.0 }}
|
22
|
+
@h = from_array(@pixels)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Our budget is spent on two square matrices:
|
26
|
+
# one with the differences from row to row
|
27
|
+
# the second with differences from column to column
|
28
|
+
|
29
|
+
def dimension(size)
|
30
|
+
Math.sqrt(size / 2).to_i
|
31
|
+
end
|
32
|
+
|
33
|
+
def size=(newsize)
|
34
|
+
fail(ArgumentError, "size must be integer, got %s: %s" % [newsize, newsize.class]) unless newsize.is_a?(Numeric)
|
35
|
+
fail(ArgumentError, "size must be integer, got %s: %s" % [newsize, newsize.class]) unless newsize.integer?
|
36
|
+
fail(ArgumentError, "size must be >= 4") unless newsize >= 4
|
37
|
+
if self.dimension(newsize) != Math.sqrt(newsize / 2.0)
|
38
|
+
fail(ArgumentError, "sqrt(size / 2) must be integer, is sqrt(%s / 2) / 2 = %s" % [newsize, self.dimension(newsize)])
|
9
39
|
end
|
10
|
-
@
|
40
|
+
@size = newsize
|
41
|
+
@dimension = self.dimension(@size)
|
11
42
|
end
|
43
|
+
|
44
|
+
# (Hamming-) Distance, or visual difference
|
45
|
+
#
|
46
|
+
# @param [DHashy] another hash
|
47
|
+
# @return [Integer] The Hamming distance (also Manhattan-distance),
|
48
|
+
# i. e. the number of different bits
|
49
|
+
|
12
50
|
def -(other)
|
13
|
-
(0
|
51
|
+
[0,1].map { |matrix| (0...@dimension).map {|x| (0...@dimension).map {|y| (@h[matrix][x][y] == other[matrix][x][y]) ? 0 : 1 }.sum}.sum}.sum
|
14
52
|
end
|
53
|
+
|
15
54
|
def[](index)
|
16
|
-
@
|
55
|
+
@h[index]
|
17
56
|
end
|
18
|
-
|
57
|
+
|
58
|
+
# Visual Equality
|
59
|
+
#
|
60
|
+
# @oaram [DHashy] another hash
|
61
|
+
# @param [cutoff] number of bits allowed to differ.
|
62
|
+
# Default: @size / 64 (i. e. 2 bits for default size)
|
63
|
+
# @return [Boolean]
|
64
|
+
def ==(other, cutoff=@size / 64)
|
19
65
|
self - other < 5
|
20
66
|
end
|
21
67
|
|
22
|
-
def display
|
23
|
-
|
68
|
+
def display(values = nil)
|
69
|
+
values = @h unless values
|
70
|
+
[1,0].map do |matrix|
|
71
|
+
(0...@dimension).map do |x|
|
72
|
+
(0...@dimension).map do |y|
|
73
|
+
values[matrix][x][y] ? '*' : '.'
|
74
|
+
end.join(" ")
|
75
|
+
end.join("\n")
|
76
|
+
end.join("\n\n") + "\n"
|
77
|
+
end
|
78
|
+
|
79
|
+
# return [String] The concatenated row- and
|
80
|
+
# column-wise matrices, compatible to the
|
81
|
+
# python implementation
|
82
|
+
# @see https://github.com/Jetsetter/dhash
|
83
|
+
def matrix
|
84
|
+
[0,1].map do |matrix|
|
85
|
+
(0...@dimension).map do |x|
|
86
|
+
(0...@dimension).map do |y|
|
87
|
+
@h[matrix][x][y] ? '1' : '0'
|
88
|
+
end.join
|
89
|
+
end.join("\n")
|
90
|
+
end.join("\n")
|
24
91
|
end
|
92
|
+
|
25
93
|
def to_s
|
26
|
-
|
94
|
+
[0,1].map do |matrix|
|
95
|
+
(0...@dimension).map do |x|
|
96
|
+
(0...@dimension).map do |y|
|
97
|
+
@h[matrix][x][y] ? 1 : 0
|
98
|
+
end.join
|
99
|
+
end.join
|
100
|
+
end.join
|
27
101
|
end
|
28
102
|
|
103
|
+
# return [Integer] The integer of bit length
|
104
|
+
# <size> representing the hash
|
29
105
|
def to_i
|
30
|
-
|
106
|
+
[0,1].map do |matrix|
|
107
|
+
(0...@dimension).map do |x|
|
108
|
+
(0...@dimension).map do |y|
|
109
|
+
@h[x][y] * 2**(x*y)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
.sum
|
31
114
|
end
|
32
|
-
|
33
|
-
|
34
|
-
def from_array(
|
35
|
-
|
115
|
+
|
116
|
+
private
|
117
|
+
def from_array(gray)
|
118
|
+
[:rows, :cols].map do |direction|
|
119
|
+
(0...@dimension).map do |y|
|
120
|
+
(0...@dimension).map do |x|
|
121
|
+
if direction == :rows
|
122
|
+
gray[x][y] <= gray[x+1][y]
|
123
|
+
else
|
124
|
+
gray[x][y] <= gray[x][y+1]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
36
128
|
end
|
37
|
-
|
129
|
+
end
|
38
130
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dhashy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthias Winkelmann
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-07-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mini_magick
|
@@ -16,42 +16,42 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '4
|
19
|
+
version: '4'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '4
|
26
|
+
version: '4'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 1.16
|
33
|
+
version: '1.16'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 1.16
|
40
|
+
version: '1.16'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rake
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '12.3'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '12.3'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: minitest
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -105,7 +105,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
105
105
|
version: '0'
|
106
106
|
requirements: []
|
107
107
|
rubyforge_project:
|
108
|
-
rubygems_version: 2.
|
108
|
+
rubygems_version: 2.7.7
|
109
109
|
signing_key:
|
110
110
|
specification_version: 4
|
111
111
|
summary: Perceptive Image Hashing and Deduplication.
|