pHash 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/.gitignore +12 -0
  2. data/LICENSE.txt +20 -0
  3. data/README.markdown +53 -0
  4. data/audiophash.diff +17 -0
  5. data/lib/phash.rb +44 -0
  6. data/lib/phash/all.rb +3 -0
  7. data/lib/phash/audio.rb +116 -0
  8. data/lib/phash/image.rb +59 -0
  9. data/lib/phash/text.rb +100 -0
  10. data/lib/phash/video.rb +55 -0
  11. data/pHash.gemspec +20 -0
  12. data/spec/data/audiophash.cpp-0.9.3.txt +571 -0
  13. data/spec/data/audiophash.cpp-0.9.4.txt +572 -0
  14. data/spec/data/audiophash.h-0.9.3.txt +111 -0
  15. data/spec/data/audiophash.h-0.9.4.txt +108 -0
  16. data/spec/data/hal9000-m.mp3 +0 -0
  17. data/spec/data/hal9000-o.mp3 +0 -0
  18. data/spec/data/jug-0-10.jpg +0 -0
  19. data/spec/data/jug-0-120.png +0 -0
  20. data/spec/data/jug-0-50.jpg +0 -0
  21. data/spec/data/jug-0-70.jpg +0 -0
  22. data/spec/data/jug-1-10.jpg +0 -0
  23. data/spec/data/jug-1-120.png +0 -0
  24. data/spec/data/jug-1-50.jpg +0 -0
  25. data/spec/data/jug-1-70.jpg +0 -0
  26. data/spec/data/jug-120.mp4 +0 -0
  27. data/spec/data/jug-150.mp4 +0 -0
  28. data/spec/data/jug-180.mp4 +0 -0
  29. data/spec/data/jug-2-10.jpg +0 -0
  30. data/spec/data/jug-2-120.png +0 -0
  31. data/spec/data/jug-2-50.jpg +0 -0
  32. data/spec/data/jug-2-70.jpg +0 -0
  33. data/spec/data/mouse-0-10.jpg +0 -0
  34. data/spec/data/mouse-0-120.png +0 -0
  35. data/spec/data/mouse-0-50.jpg +0 -0
  36. data/spec/data/mouse-0-70.jpg +0 -0
  37. data/spec/data/mouse-1-10.jpg +0 -0
  38. data/spec/data/mouse-1-120.png +0 -0
  39. data/spec/data/mouse-1-50.jpg +0 -0
  40. data/spec/data/mouse-1-70.jpg +0 -0
  41. data/spec/data/mouse-120.mp4 +0 -0
  42. data/spec/data/mouse-150.mp4 +0 -0
  43. data/spec/data/mouse-180.mp4 +0 -0
  44. data/spec/data/mouse-2-10.jpg +0 -0
  45. data/spec/data/mouse-2-120.png +0 -0
  46. data/spec/data/mouse-2-50.jpg +0 -0
  47. data/spec/data/mouse-2-70.jpg +0 -0
  48. data/spec/data/scream-m.mp3 +0 -0
  49. data/spec/data/scream-o.mp3 +0 -0
  50. data/spec/data/vader-m.mp3 +0 -0
  51. data/spec/data/vader-o.mp3 +0 -0
  52. data/spec/phash_spec.rb +43 -0
  53. data/spec/spec_helper.rb +10 -0
  54. metadata +186 -0
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /pkg/
2
+ /*.gem
3
+
4
+ /doc/
5
+ /rdoc/
6
+ /.yardoc/
7
+ /coverage/
8
+
9
+ Makefile
10
+ *.o
11
+ *.bundle
12
+ /tmp/
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Ivan Kuchin
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,53 @@
1
+ # pHash
2
+
3
+ Interface to [pHash](http://pHash.org/).
4
+
5
+ ## Installation
6
+
7
+ gem install pHash
8
+
9
+ Audio hash functions needs to be compiled with C linkage, so if you get `FFI::NotFoundError` check names of methods in `libpHash`. Tiny patch for pHash 0.9.4 is in `audiophash.diff`.
10
+
11
+ ## Usage
12
+
13
+ Compare two mp3s:
14
+
15
+ require 'phash/audio'
16
+
17
+ a = Phash::Audio.new('first.mp3')
18
+ b = Phash::Audio.new('second.mp3')
19
+ a.similarity(b)
20
+
21
+ or just
22
+
23
+ a % b
24
+
25
+ Get bunch of comparators and work with them:
26
+
27
+ audios = Phash::Audio.for_paths(Dir['**/*.{mp3,wav}'])
28
+ audios.combination(2) do |a, b|
29
+ similarity = a % b
30
+ # work with similarity
31
+ end
32
+
33
+ Videos:
34
+
35
+ require 'phash/video'
36
+
37
+ Phash::Video.new('first.mp4') % Phash::Video.new('second.mp4')
38
+
39
+ Images:
40
+
41
+ require 'phash/image'
42
+
43
+ Phash::Image.new('first.jpg') % Phash::Image.new('second.png')
44
+
45
+ Texts:
46
+
47
+ require 'phash/text'
48
+
49
+ Phash::Text.new('first.txt') % Phash::Text.new('second.txt')
50
+
51
+ ## Copyright
52
+
53
+ Copyright (c) 2011 Ivan Kuchin. See LICENSE.txt for details.
data/audiophash.diff ADDED
@@ -0,0 +1,17 @@
1
+ --- src/audiophash.h.orig 2011-12-13 14:47:08.000000000 +0100
2
+ +++ src/audiophash.h 2011-12-13 14:47:12.000000000 +0100
3
+ @@ -34,7 +34,6 @@
4
+
5
+ extern "C" {
6
+ #include "ph_fft.h"
7
+ -}
8
+
9
+ /* /brief count number of samples in file
10
+ *
11
+ @@ -105,4 +104,6 @@
12
+ */
13
+ double* ph_audio_distance_ber(uint32_t *hash_a , const int Na, uint32_t *hash_b, const int Nb, const float threshold, const int block_size, int &Nc);
14
+
15
+ +}
16
+ +
17
+ #endif
data/lib/phash.rb ADDED
@@ -0,0 +1,44 @@
1
+ require 'ffi'
2
+
3
+ module Phash
4
+ class HashData
5
+ attr_reader :data, :length
6
+ def initialize(data, length = nil)
7
+ @data, @length = data, length
8
+ end
9
+ end
10
+
11
+ class FileHash
12
+ attr_reader :path
13
+
14
+ # File path
15
+ def initialize(path)
16
+ @path = path
17
+ end
18
+
19
+ # Init multiple image instances
20
+ def self.for_paths(paths, *args)
21
+ paths.map do |path|
22
+ new(path, *args)
23
+ end
24
+ end
25
+
26
+ # Cached hash of text
27
+ def phash
28
+ @phash ||= compute_phash
29
+ end
30
+
31
+ def %(other)
32
+ similarity(other)
33
+ end
34
+ end
35
+
36
+ extend FFI::Library
37
+
38
+ ffi_lib(ENV['PHASH_LIB'] || Dir['/{usr,usr/local,opt/local}/lib/libpHash.{dylib,so}'].first)
39
+
40
+ autoload :Audio, 'phash/audio'
41
+ autoload :Image, 'phash/image'
42
+ autoload :Text, 'phash/text'
43
+ autoload :Video, 'phash/video'
44
+ end
data/lib/phash/all.rb ADDED
@@ -0,0 +1,3 @@
1
+ %w[audio image text video].each do |type|
2
+ require "phash/#{type}"
3
+ end
@@ -0,0 +1,116 @@
1
+ require 'phash'
2
+
3
+ module Phash
4
+ # read audio
5
+ #
6
+ # param filename - path and name of audio file to read
7
+ # param sr - sample rate conversion
8
+ # param channels - nb channels to convert to (always 1) unused
9
+ # param buf - preallocated buffer
10
+ # param buflen - (in/out) param for buf length
11
+ # param nbsecs - float value for duration (in secs) to read from file
12
+ #
13
+ # return float* - float pointer to start of buffer - one channel of audio, NULL if error
14
+ #
15
+ # float* ph_readaudio(const char *filename, int sr, int channels, float *sigbuf, int &buflen, const float nbsecs = 0);
16
+ #
17
+ attach_function :ph_readaudio, [:string, :int, :int, :pointer, :pointer, :float], :pointer
18
+
19
+ # audio hash calculation
20
+ # purpose: hash calculation for each frame in the buffer.
21
+ # Each value is computed from successive overlapping frames of the input buffer.
22
+ # The value is based on the bark scale values of the frame fft spectrum. The value
23
+ # computed from temporal and spectral differences on the bark scale.
24
+ #
25
+ # param buf - pointer to start of buffer
26
+ # param N - length of buffer
27
+ # param sr - sample rate on which to base the audiohash
28
+ # param nb_frames - (out) number of frames in audio buf and length of audiohash buffer returned
29
+ #
30
+ # return uint32 pointer to audio hash, NULL for error
31
+ #
32
+ # uint32_t* ph_audiohash(float *buf, int nbbuf, const int sr, int &nbframes);
33
+ #
34
+ attach_function :ph_audiohash, [:pointer, :int, :int, :pointer], :pointer
35
+
36
+ # distance function between two hashes
37
+ #
38
+ # param hash_a - first hash
39
+ # param Na - length of first hash
40
+ # param hash_b - second hash
41
+ # param Nb - length of second hash
42
+ # param threshold - threshold value to compare successive blocks, 0.25, 0.30, 0.35
43
+ # param block_size - length of block_size, 256
44
+ # param Nc - (out) length of confidence score vector
45
+ #
46
+ # return double - ptr to confidence score vector
47
+ #
48
+ # double* ph_audio_distance_ber(uint32_t *hash_a, const int Na, uint32_t *hash_b, const int Nb, const float threshold, const int block_size, int &Nc);
49
+ #
50
+ attach_function :ph_audio_distance_ber, [:pointer, :int, :pointer, :int, :float, :int, :pointer], :pointer
51
+
52
+ class << self
53
+ class AudioHash < HashData; end
54
+
55
+ # Read audio file specified by path and optional length using <tt>ph_readaudio</tt> and get its hash using <tt>ph_audiohash</tt>
56
+ def audio_hash(path, length = 0)
57
+ sample_rate = 8000
58
+ audio_data_length_p = FFI::MemoryPointer.new :int
59
+ if audio_data = ph_readaudio(path.to_s, sample_rate, 1, nil, audio_data_length_p, length.to_f)
60
+ audio_data_length = audio_data_length_p.get_int(0)
61
+ audio_data_length_p.free
62
+
63
+ hash_data_length_p = FFI::MemoryPointer.new :int
64
+ if hash_data = ph_audiohash(audio_data, audio_data_length, sample_rate, hash_data_length_p)
65
+ hash_data_length = hash_data_length_p.get_int(0)
66
+ hash_data_length_p.free
67
+
68
+ AudioHash.new(hash_data, hash_data_length)
69
+ end
70
+ end
71
+ end
72
+
73
+ # Get distance between two audio hashes using <tt>ph_audio_distance_ber</tt>
74
+ def audio_distance_ber(hash_a, hash_b, threshold = 0.25, block_size = 256)
75
+ hash_a.is_a?(AudioHash) or raise ArgumentError.new('hash_a is not an AudioHash')
76
+ hash_b.is_a?(AudioHash) or raise ArgumentError.new('hash_b is not an AudioHash')
77
+
78
+ distance_vector_length_p = FFI::MemoryPointer.new :int
79
+ block_size = [block_size.to_i, hash_a.length, hash_b.length].min
80
+ if distance_vector = ph_audio_distance_ber(hash_a.data, hash_a.length, hash_b.data, hash_b.length, threshold.to_f, block_size, distance_vector_length_p)
81
+ distance_vector_length = distance_vector_length_p.get_int(0)
82
+ distance_vector_length_p.free
83
+
84
+ distance = distance_vector.get_array_of_double(0, distance_vector_length)
85
+ distance_vector.free
86
+ distance
87
+ end
88
+ end
89
+
90
+ # Get similarity from audio_distance_ber
91
+ def audio_similarity(hash_a, hash_b, *args)
92
+ audio_distance_ber(hash_a, hash_b, *args).max
93
+ end
94
+ end
95
+
96
+ # Class to store audio file hash and compare to other
97
+ class Audio < FileHash
98
+ attr_reader :length
99
+
100
+ # Audio path and optional length in seconds to read
101
+ def initialize(path, length = 0)
102
+ @path, @length = path, length
103
+ end
104
+
105
+ # Similarity with other audio
106
+ def similarity(other, *args)
107
+ Phash.audio_similarity(phash, other.phash, *args)
108
+ end
109
+
110
+ private
111
+
112
+ def compute_phash
113
+ Phash.audio_hash(@path, @length)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,59 @@
1
+ require 'phash'
2
+
3
+ module Phash
4
+ # compute dct robust image hash
5
+ #
6
+ # param file string variable for name of file
7
+ # param hash of type ulong64 (must be 64-bit variable)
8
+ # return int value - -1 for failure, 1 for success
9
+ #
10
+ # int ph_dct_imagehash(const char* file, ulong64 &hash);
11
+ #
12
+ attach_function :ph_dct_imagehash, [:string, :pointer], :int
13
+
14
+ # no info in pHash.h
15
+ #
16
+ # int ph_hamming_distance(const ulong64 hash1,const ulong64 hash2);
17
+ #
18
+ attach_function :ph_hamming_distance, [:uint64, :uint64], :int
19
+
20
+ class << self
21
+ # Get image file hash using <tt>ph_dct_imagehash</tt>
22
+ def image_hash(path)
23
+ hash_p = FFI::MemoryPointer.new :ulong_long
24
+ if -1 != ph_dct_imagehash(path.to_s, hash_p)
25
+ hash = hash_p.get_uint64(0)
26
+ hash_p.free
27
+
28
+ hash
29
+ end
30
+ end
31
+
32
+ # Get distance between two image hashes using <tt>ph_hamming_distance</tt>
33
+ def image_hamming_distance(hash_a, hash_b)
34
+ hash_a.is_a?(Integer) or raise ArgumentError.new('hash_a is not an Integer')
35
+ hash_b.is_a?(Integer) or raise ArgumentError.new('hash_b is not an Integer')
36
+
37
+ ph_hamming_distance(hash_a, hash_b)
38
+ end
39
+
40
+ # Get similarity from hamming_distance
41
+ def image_similarity(hash_a, hash_b)
42
+ 1 - image_hamming_distance(hash_a, hash_b) / 64.0
43
+ end
44
+ end
45
+
46
+ # Class to store image file hash and compare to other
47
+ class Image < FileHash
48
+ # Similarity with other image
49
+ def similarity(other)
50
+ Phash.image_similarity(phash, other.phash)
51
+ end
52
+
53
+ private
54
+
55
+ def compute_phash
56
+ Phash.image_hash(@path)
57
+ end
58
+ end
59
+ end
data/lib/phash/text.rb ADDED
@@ -0,0 +1,100 @@
1
+ require 'phash'
2
+
3
+ module Phash
4
+ class TxtHashPoint < FFI::Struct
5
+ layout :hash, :uint64,
6
+ :index, :off_t
7
+ end
8
+
9
+ class TxtMatch < FFI::Struct
10
+ layout :index_a, :off_t,
11
+ :index_b, :off_t,
12
+ :length, :uint32
13
+ end
14
+
15
+ # textual hash for file
16
+ #
17
+ # param filename - char* name of file
18
+ # param nbpoints - int length of array of return value (out)
19
+ # return TxtHashPoint* array of hash points with respective index into file.
20
+ #
21
+ # TxtHashPoint* ph_texthash(const char *filename, int *nbpoints);
22
+ #
23
+ attach_function :ph_texthash, [:string, :pointer], :pointer
24
+
25
+ # compare 2 text hashes
26
+ #
27
+ # param hash1 -TxtHashPoint
28
+ # param N1 - int length of hash1
29
+ # param hash2 - TxtHashPoint
30
+ # param N2 - int length of hash2
31
+ # param nbmatches - int number of matches found (out)
32
+ # return TxtMatch* - list of all matches
33
+ #
34
+ # TxtMatch* ph_compare_text_hashes(TxtHashPoint *hash1, int N1, TxtHashPoint *hash2, int N2, int *nbmatches);
35
+ #
36
+ attach_function :ph_compare_text_hashes, [:pointer, :int, :pointer, :int, :pointer], :pointer
37
+
38
+ class << self
39
+ class TextHash < HashData; end
40
+
41
+ # Get text file hash using <tt>ph_texthash</tt>
42
+ def text_hash(path)
43
+ hash_data_length_p = FFI::MemoryPointer.new :int
44
+ if hash_data = ph_texthash(path.to_s, hash_data_length_p)
45
+ hash_data_length = hash_data_length_p.get_int(0)
46
+ hash_data_length_p.free
47
+
48
+ TextHash.new(hash_data, hash_data_length)
49
+ end
50
+ end
51
+
52
+ # Get distance between two text hashes using <tt>text_distance</tt>
53
+ def text_hash_matches(hash_a, hash_b)
54
+ hash_a.is_a?(TextHash) or raise ArgumentError.new('hash_a is not a TextHash')
55
+ hash_b.is_a?(TextHash) or raise ArgumentError.new('hash_b is not a TextHash')
56
+
57
+ matches_length_p = FFI::MemoryPointer.new :int
58
+ if data = ph_compare_text_hashes(hash_a.data, hash_a.length, hash_b.data, hash_b.length, matches_length_p)
59
+ matches_length = matches_length_p.get_int(0)
60
+ matches_length_p.free
61
+
62
+ matches = matches_length.times.map{ |i| TxtMatch.new(data + i * TxtMatch.size) }
63
+ data.free
64
+ matches
65
+ end
66
+ end
67
+
68
+ def text_similarity(hash_a, hash_b)
69
+ matches = text_hash_matches(hash_a, hash_b)
70
+ # p [hash_a.length, hash_b.length, matches.length]
71
+ matched_a = Array.new(hash_a.length)
72
+ matched_b = Array.new(hash_b.length)
73
+ matches.each do |match|
74
+ index_a = match[:index_a]
75
+ index_b = match[:index_b]
76
+ match[:length].times do |i|
77
+ matched_a[index_a + i] = true
78
+ matched_b[index_b + i] = true
79
+ end
80
+ end
81
+ coverage_a = matched_a.nitems / hash_a.length.to_f
82
+ coverage_b = matched_b.nitems / hash_b.length.to_f
83
+ (coverage_a + coverage_b) * 0.5
84
+ end
85
+ end
86
+
87
+ # Class to store text file hash and compare to other
88
+ class Text < FileHash
89
+ # Distance from other file, for now bit useless thing
90
+ def similarity(other)
91
+ Phash.text_similarity(phash, other.phash)
92
+ end
93
+
94
+ private
95
+
96
+ def compute_phash
97
+ Phash.text_hash(@path)
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,55 @@
1
+ require 'phash'
2
+
3
+ module Phash
4
+ # no info in pHash.h
5
+ #
6
+ # ulong64* ph_dct_videohash(const char *filename, int &Length);
7
+ #
8
+ attach_function :ph_dct_videohash, [:string, :pointer], :pointer
9
+
10
+ # no info in pHash.h
11
+ #
12
+ # double ph_dct_videohash_dist(ulong64 *hashA, int N1, ulong64 *hashB, int N2, int threshold=21);
13
+ #
14
+ attach_function :ph_dct_videohash_dist, [:pointer, :int, :pointer, :int, :int], :double
15
+
16
+ class << self
17
+ class VideoHash < HashData; end
18
+
19
+ # Get video hash using <tt>ph_dct_videohash</tt>
20
+ def video_hash(path)
21
+ hash_data_length_p = FFI::MemoryPointer.new :int
22
+ if hash_data = ph_dct_videohash(path.to_s, hash_data_length_p)
23
+ hash_data_length = hash_data_length_p.get_int(0)
24
+ hash_data_length_p.free
25
+
26
+ VideoHash.new(hash_data, hash_data_length)
27
+ end
28
+ end
29
+
30
+ # Get distance between two video hashes using <tt>ph_dct_videohash_dist</tt>
31
+ def video_dct_distance(hash_a, hash_b, threshold = 21)
32
+ hash_a.is_a?(VideoHash) or raise ArgumentError.new('hash_a is not a VideoHash')
33
+ hash_b.is_a?(VideoHash) or raise ArgumentError.new('hash_b is not a VideoHash')
34
+
35
+ ph_dct_videohash_dist(hash_a.data, hash_a.length, hash_b.data, hash_b.length, threshold.to_i)
36
+ end
37
+
38
+ # Get similarity from video_dct_distance
39
+ alias_method :video_similarity, :video_dct_distance
40
+ end
41
+
42
+ # Class to store video file hash and compare to other
43
+ class Video < FileHash
44
+ # Similarity with other video
45
+ def similarity(other, *args)
46
+ Phash.video_similarity(phash, other.phash, *args)
47
+ end
48
+
49
+ private
50
+
51
+ def compute_phash
52
+ Phash.video_hash(@path)
53
+ end
54
+ end
55
+ end