pHash 1.0.0

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.
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