ph 0.0.1

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 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: []