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 +7 -0
- data/Makefile +13 -0
- data/lib/ph.rb +125 -0
- data/ph.gemspec +15 -0
- data/spec/image.jpg +0 -0
- data/spec/ph_spec.rb +33 -0
- data/spec/spec_helper.rb +3 -0
- metadata +63 -0
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
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
|
data/spec/spec_helper.rb
ADDED
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: []
|