dhashy 1.0.0 → 1.0.2
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 +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.
|