ph 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 88a7e900bca90ab2c70029559c46848add5f8384a2c60040f012b1ac16691024
4
+ data.tar.gz: 4f38f845987a7c33e6d8adb70766e0ea0a0f9758a0efe0e01655370f488f0b96
5
+ SHA512:
6
+ metadata.gz: 47c53e0c8a34d0a5c1cbde470000b5de32a246dae9d3f15582ed024b9aaed938fdfcb8b141d5ec845b505765a2ccbab1b46f31b6173886d00c3ff3fcfc1fd7e2
7
+ data.tar.gz: 76d82783e82eb7f8d321a79dd21e6a72cafc5014381a6498dcb217f3a29c3c9ab3f1ec9fbf8b4099e31677e1919b4ee1f836e10f520c574a966f06c3762dea86
data/Makefile ADDED
@@ -0,0 +1,13 @@
1
+ .PHONY: *
2
+
3
+ default: test
4
+
5
+ build:
6
+ rm -f *.gem
7
+ gem build ph.gemspec
8
+
9
+ publish: build
10
+ gem push *.gem
11
+
12
+ test:
13
+ ruby -Ilib:spec spec/*_spec.rb
data/lib/ph.rb ADDED
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ class PH
4
+ attr_reader :pixels, :size
5
+
6
+ def initialize(pixels_2d, size: 64)
7
+ @pixels = pixels_2d
8
+ @size = size
9
+ end
10
+
11
+ def hash
12
+ # Binary to hex string
13
+ vector.join.to_i(2).to_s(16)
14
+ end
15
+
16
+ def vector
17
+ @_vector ||= begin
18
+ # Get DCT2D of the pixels
19
+ dct_pixels = dct2d(pixels)
20
+ # Get high frequency corner
21
+ sqrt = Math.sqrt(size).to_i
22
+ coords = Array.new(2, sqrt)
23
+ corner = flat1d(coords, dct_pixels)
24
+ corner_size = corner.length
25
+ # Median values
26
+ med = median(corner)
27
+
28
+ result = Array.new(size, 0)
29
+
30
+ corner.each.with_index do |f, i|
31
+ # Compare each value to the median
32
+ result[i] = 1 if f > med
33
+ end
34
+
35
+ result
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # DCT on a bidimensional plane
42
+ # Thank you matlab forum for explaining that it's just transpositions
43
+ # https://www.mathworks.com/matlabcentral/answers/405088-how-to-implement-dct2-in-matlab-coder
44
+ #
45
+ def dct2d(pixels_2d)
46
+ dct_fn = -> (px) { dct(px) }
47
+
48
+ pixels_2d
49
+ .map(&dct_fn).transpose
50
+ .map(&dct_fn).transpose
51
+ end
52
+
53
+ # Slices 2D vector into a 1D version
54
+ #
55
+ def flat1d(coords, pixels_2d)
56
+ slice = []
57
+ x, y = coords
58
+
59
+ Array.new(x) do |i|
60
+ Array.new(y) do |j|
61
+ slice << pixels_2d[i][j]
62
+ end
63
+ end
64
+
65
+ slice
66
+ end
67
+
68
+ # Quickselect median.
69
+ # https://rcoh.me/posts/linear-time-median-finding/
70
+ #
71
+ def median(vector)
72
+ return nil if vector.empty?
73
+
74
+ n = vector.size
75
+
76
+ return quickselect(vector, n / 2) if n.odd?
77
+ 0.5 * (quickselect(vector, n / 2 - 1) + quickselect(vector, n / 2))
78
+ end
79
+
80
+ def quickselect(vector, pos)
81
+ return vector.first if vector.size == 1 && pos.zero?
82
+
83
+ pivot = vector.sample
84
+
85
+ lows = vector.select { |i| i < pivot }
86
+ highs = vector.select { |i| i > pivot }
87
+ pivots = vector.select { |i| i == pivot }
88
+
89
+ return quickselect(lows, pos) if pos < lows.size
90
+ return pivots.first if pos < pivots.size + lows.size
91
+
92
+ quickselect(highs, pos - lows.size - pivots.size)
93
+ end
94
+
95
+ # 1984 Lee DCT implementation
96
+ #
97
+ def dct(vector)
98
+ n = vector.size
99
+ return vector if n == 1
100
+ raise StandardError, "Must be nxn" if n.zero? || n.odd?
101
+
102
+ half = n / 2
103
+ alpha = []
104
+ beta = []
105
+
106
+ Array.new(half).each.with_index do |_, i|
107
+ alpha << (vector[i] + vector[-(i + 1)])
108
+ beta << (vector[i] - vector[-(i + 1)]) / (Math.cos((i + 0.5) * Math::PI / n) * 2.0)
109
+ end
110
+
111
+ alpha = dct(alpha)
112
+ beta = dct(beta)
113
+ result = []
114
+
115
+ Array.new(half - 1).each.with_index do |_, i|
116
+ result << alpha[i]
117
+ result << beta[i] + beta[i + 1]
118
+ end
119
+
120
+ result << alpha[-1]
121
+ result << beta[-1]
122
+
123
+ result
124
+ end
125
+ end
data/ph.gemspec ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "ph"
5
+ s.version = "0.0.1"
6
+ s.summary = "Perceptual Hashing"
7
+ s.authors = ["elcuervo"]
8
+ s.licenses = %w[MIT]
9
+ s.email = ["elcuervo@elcuervo.net"]
10
+ s.homepage = "http://github.com/elcuervo/ph"
11
+ s.files = `git ls-files`.split("\n")
12
+ s.test_files = `git ls-files test`.split("\n")
13
+
14
+ s.add_development_dependency("vips", "~> 8.10")
15
+ end
data/spec/image.jpg ADDED
Binary file
data/spec/ph_spec.rb ADDED
@@ -0,0 +1,33 @@
1
+ require "spec_helper"
2
+
3
+ describe PH do
4
+ let(:file) { "spec/image.jpg" }
5
+ let(:size) { 64 }
6
+
7
+ before do
8
+ img = Vips::Image.new_from_file(file)
9
+ width, height = img.width, img.height
10
+ w_scale, v_scale = size.fdiv(width), size.fdiv(height)
11
+
12
+ # Rescale to 64x64 and greyscale
13
+ img = img.resize(w_scale, vscale: v_scale).colourspace(:grey16)
14
+ # Ensure a 2D plane
15
+ @pixels_2d = img.to_a.map(&:flatten)
16
+ end
17
+
18
+ subject { PH.new(@pixels_2d) }
19
+
20
+ it "#hash" do
21
+ assert_equal "859091ce633aaebb", subject.hash
22
+ end
23
+
24
+ it "#vector" do
25
+ vector = [
26
+ 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1,
27
+ 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0,
28
+ 1, 1
29
+ ]
30
+
31
+ assert_equal vector, subject.vector
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ require "minitest/autorun"
2
+ require "vips"
3
+ require "ph"
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ph
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - elcuervo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-04-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: vips
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '8.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '8.10'
27
+ description:
28
+ email:
29
+ - elcuervo@elcuervo.net
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Makefile
35
+ - lib/ph.rb
36
+ - ph.gemspec
37
+ - spec/image.jpg
38
+ - spec/ph_spec.rb
39
+ - spec/spec_helper.rb
40
+ homepage: http://github.com/elcuervo/ph
41
+ licenses:
42
+ - MIT
43
+ metadata: {}
44
+ post_install_message:
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubygems_version: 3.0.3
60
+ signing_key:
61
+ specification_version: 4
62
+ summary: Perceptual Hashing
63
+ test_files: []