pHash 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +12 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +53 -0
- data/audiophash.diff +17 -0
- data/lib/phash.rb +44 -0
- data/lib/phash/all.rb +3 -0
- data/lib/phash/audio.rb +116 -0
- data/lib/phash/image.rb +59 -0
- data/lib/phash/text.rb +100 -0
- data/lib/phash/video.rb +55 -0
- data/pHash.gemspec +20 -0
- data/spec/data/audiophash.cpp-0.9.3.txt +571 -0
- data/spec/data/audiophash.cpp-0.9.4.txt +572 -0
- data/spec/data/audiophash.h-0.9.3.txt +111 -0
- data/spec/data/audiophash.h-0.9.4.txt +108 -0
- data/spec/data/hal9000-m.mp3 +0 -0
- data/spec/data/hal9000-o.mp3 +0 -0
- data/spec/data/jug-0-10.jpg +0 -0
- data/spec/data/jug-0-120.png +0 -0
- data/spec/data/jug-0-50.jpg +0 -0
- data/spec/data/jug-0-70.jpg +0 -0
- data/spec/data/jug-1-10.jpg +0 -0
- data/spec/data/jug-1-120.png +0 -0
- data/spec/data/jug-1-50.jpg +0 -0
- data/spec/data/jug-1-70.jpg +0 -0
- data/spec/data/jug-120.mp4 +0 -0
- data/spec/data/jug-150.mp4 +0 -0
- data/spec/data/jug-180.mp4 +0 -0
- data/spec/data/jug-2-10.jpg +0 -0
- data/spec/data/jug-2-120.png +0 -0
- data/spec/data/jug-2-50.jpg +0 -0
- data/spec/data/jug-2-70.jpg +0 -0
- data/spec/data/mouse-0-10.jpg +0 -0
- data/spec/data/mouse-0-120.png +0 -0
- data/spec/data/mouse-0-50.jpg +0 -0
- data/spec/data/mouse-0-70.jpg +0 -0
- data/spec/data/mouse-1-10.jpg +0 -0
- data/spec/data/mouse-1-120.png +0 -0
- data/spec/data/mouse-1-50.jpg +0 -0
- data/spec/data/mouse-1-70.jpg +0 -0
- data/spec/data/mouse-120.mp4 +0 -0
- data/spec/data/mouse-150.mp4 +0 -0
- data/spec/data/mouse-180.mp4 +0 -0
- data/spec/data/mouse-2-10.jpg +0 -0
- data/spec/data/mouse-2-120.png +0 -0
- data/spec/data/mouse-2-50.jpg +0 -0
- data/spec/data/mouse-2-70.jpg +0 -0
- data/spec/data/scream-m.mp3 +0 -0
- data/spec/data/scream-o.mp3 +0 -0
- data/spec/data/vader-m.mp3 +0 -0
- data/spec/data/vader-o.mp3 +0 -0
- data/spec/phash_spec.rb +43 -0
- data/spec/spec_helper.rb +10 -0
- metadata +186 -0
data/.gitignore
ADDED
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
data/lib/phash/audio.rb
ADDED
@@ -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
|
data/lib/phash/image.rb
ADDED
@@ -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
|
data/lib/phash/video.rb
ADDED
@@ -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
|