dcthash 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +63 -0
- data/lib/dcthash/version.rb +5 -0
- data/lib/dcthash.rb +94 -0
- data/sig/dcthash.rbs +18 -0
- data/sig/rmagick.rbs +18 -0
- metadata +66 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b73e184bb46162ad4bc3de29888ba7b427c5a28ffd8ab3b5a7028f601868c7fd
|
4
|
+
data.tar.gz: a89de0b14f3498f3ca4c6ca94123b44798d998c8a6163130c57ca340d80198d5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b33b7d04d8441c142d8f65853d29e59fbca08b9cf8cbea499081f1eba228e4b09dae225ebbac3c81f821a1c81c46320dba748d30c514b2521bbe015c33b338d7
|
7
|
+
data.tar.gz: 3ec6099076b8bb4c418b38cae43aab403e5bc8ce7675198314d95d5e050abd6a92a54ffb03fabe65017e59e4b5a09a59c174104ef802925b60ed108a2a8111f1
|
data/README.md
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# DCTHash
|
2
|
+
|
3
|
+
This is Ruby Gem that can be used to produce and compare perceptual image hashes.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
### Prerequisites
|
8
|
+
|
9
|
+
On Debian/Ubuntu/Mint, you can run:
|
10
|
+
```sh
|
11
|
+
sudo apt get install libmagickwand-dev
|
12
|
+
```
|
13
|
+
|
14
|
+
On Arch you can run:
|
15
|
+
```sh
|
16
|
+
pacman -Sy pkg-config imagemagick
|
17
|
+
```
|
18
|
+
|
19
|
+
On macOS, you can run:
|
20
|
+
```sh
|
21
|
+
brew install pkg-config imagemagick
|
22
|
+
```
|
23
|
+
|
24
|
+
### Gem Install
|
25
|
+
|
26
|
+
Install via Bundler
|
27
|
+
```sh
|
28
|
+
bundle add dcthash
|
29
|
+
```
|
30
|
+
|
31
|
+
Or install via RubyGems:
|
32
|
+
```sh
|
33
|
+
gem install dcthash
|
34
|
+
```
|
35
|
+
|
36
|
+
## Usage
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
require "dcthash"
|
40
|
+
require "rmagick"
|
41
|
+
|
42
|
+
image1 = Magick::Image.read("image1.png").first
|
43
|
+
image2 = Magick::Image.read("image2.png").first
|
44
|
+
|
45
|
+
# Generate image hashes
|
46
|
+
hash1 = Dcthash.calculate(image1)
|
47
|
+
hash2 = Dcthash.calculate(image2)
|
48
|
+
|
49
|
+
# Determine if hashes are similar
|
50
|
+
similar = Dcthash.similar?(hash1, hash2, threshold = 13)
|
51
|
+
|
52
|
+
# Calculate difference between two hashes
|
53
|
+
distance = Dcthash.distance(hash1, hash2)
|
54
|
+
```
|
55
|
+
|
56
|
+
## Contributing
|
57
|
+
|
58
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/actuallydamo/dcthash.
|
59
|
+
|
60
|
+
|
61
|
+
## License
|
62
|
+
|
63
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/lib/dcthash.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "dcthash/version"
|
4
|
+
|
5
|
+
# DCT Hash Module
|
6
|
+
module Dcthash
|
7
|
+
EDGE_SIZE = 32 # Resize each edge of image to this
|
8
|
+
SUBSAMPLE_START = 1 # Grab subsample from this point
|
9
|
+
SUBSAMPLE_END = 8 # Grab subsample to this point
|
10
|
+
|
11
|
+
# Calculate the distance between two hashes
|
12
|
+
# @param hash1 [String] The first hash
|
13
|
+
# @param hash2 [String] The second hash
|
14
|
+
# @return [Integer] hamming distance between hashes
|
15
|
+
def self.distance(hash1, hash2)
|
16
|
+
(hash1.to_i(16) ^ hash2.to_i(16)).to_s(2).count("1")
|
17
|
+
end
|
18
|
+
|
19
|
+
# Determine if two hashes are similar
|
20
|
+
# @param hash1 [String] The first hash
|
21
|
+
# @param hash2 [String] The second hash
|
22
|
+
# @param threshold [Integer] (optional) The threshold for similarity
|
23
|
+
# @return [Boolean] true if hashes are similar
|
24
|
+
def self.similar?(hash1, hash2, threshold = 13)
|
25
|
+
distance(hash1, hash2) < threshold
|
26
|
+
end
|
27
|
+
|
28
|
+
# Calculate the hash of an image
|
29
|
+
# @param image [Magick::Image] The image to hash
|
30
|
+
# @return [String] the hash of the image
|
31
|
+
def self.calculate(image)
|
32
|
+
image.resize!(EDGE_SIZE, EDGE_SIZE, Magick::PointFilter)
|
33
|
+
image = image.quantize(256, Magick::GRAYColorspace, Magick::NoDitherMethod)
|
34
|
+
|
35
|
+
intensity_matrix = image.export_pixels(0, 0, EDGE_SIZE, EDGE_SIZE, "I").each_slice(EDGE_SIZE).to_a
|
36
|
+
|
37
|
+
dct_result = dct_2d(intensity_matrix)
|
38
|
+
# @type var sub_matrix: Array[Float]
|
39
|
+
sub_matrix = ((dct_result[SUBSAMPLE_START..SUBSAMPLE_END] || [])
|
40
|
+
.transpose[SUBSAMPLE_START..SUBSAMPLE_END] || []).flatten
|
41
|
+
|
42
|
+
median = median(sub_matrix)
|
43
|
+
result = sub_matrix.map { |px| px < median ? 1 : 0 }
|
44
|
+
result.join.to_i(2).to_s(16)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Find the median from an even length array of numbers
|
48
|
+
# @param array [Array<Float>] The array of numbers
|
49
|
+
# @return [Float] the value of the median
|
50
|
+
def self.median(array)
|
51
|
+
sorted = array.sort
|
52
|
+
length = array.length
|
53
|
+
middle = length / 2
|
54
|
+
# We only use even length arrays so the following line is not needed
|
55
|
+
# return sorted[middle] if length.odd?
|
56
|
+
|
57
|
+
(sorted[middle - 1] + sorted[middle]).fdiv(2)
|
58
|
+
end
|
59
|
+
private_class_method :median
|
60
|
+
|
61
|
+
# Calculate the DCT of a vector
|
62
|
+
# Adapted from https://www.nayuki.io/page/fast-discrete-cosine-transform-algorithms
|
63
|
+
# Copyright (c) 2020 Project Nayuki. (MIT License)
|
64
|
+
# @param vector [Array<Numeric>] the vector to transform
|
65
|
+
# @return [Array<Float>] the discrete cosine transform of the vector
|
66
|
+
def self.dct(vector)
|
67
|
+
vector_size = vector.size
|
68
|
+
return [Float(vector[0])] if vector_size == 1
|
69
|
+
|
70
|
+
half = vector_size / 2
|
71
|
+
alpha = (0...half).map { |i| vector[i] + (vector[-(i + 1)]) }
|
72
|
+
beta = (0...half).map do |i|
|
73
|
+
(vector[i] - (vector[-(i + 1)])).fdiv(Math.cos(((i + 0.5) * Math::PI).fdiv(vector_size)) * 2)
|
74
|
+
end
|
75
|
+
alpha = dct(alpha)
|
76
|
+
beta = dct(beta)
|
77
|
+
result = (0...half - 1).flat_map do |i|
|
78
|
+
[alpha[i], beta[i] + beta[i + 1]]
|
79
|
+
end
|
80
|
+
result.push(alpha[-1])
|
81
|
+
result.push(beta[-1])
|
82
|
+
end
|
83
|
+
private_class_method :dct
|
84
|
+
|
85
|
+
# Calculate the 2D DCT of a matrix
|
86
|
+
# @param input [Array<Array<Numeric>>] the matrix to calculate the 2D DCT of
|
87
|
+
# @return [Array<Array<Float>>] the DCT of the matrix
|
88
|
+
def self.dct_2d(input)
|
89
|
+
input.map { |row| dct(row) }
|
90
|
+
.transpose
|
91
|
+
.map { |col| dct(col) }
|
92
|
+
end
|
93
|
+
private_class_method :dct_2d
|
94
|
+
end
|
data/sig/dcthash.rbs
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Dcthash
|
2
|
+
VERSION: String
|
3
|
+
EDGE_SIZE: Integer
|
4
|
+
SUBSAMPLE_START: Integer
|
5
|
+
SUBSAMPLE_END: Integer
|
6
|
+
|
7
|
+
def self.distance: (String, String) -> Integer
|
8
|
+
|
9
|
+
def self.similar?: (String, String, ?Integer) -> bool
|
10
|
+
|
11
|
+
def self.calculate: (Magick::Image) -> String
|
12
|
+
|
13
|
+
def self.median: (Array[Float]) -> Float
|
14
|
+
|
15
|
+
def self.dct: (Array[Numeric]) -> Array[Float]
|
16
|
+
|
17
|
+
def self.dct_2d: (Array[Array[Numeric]]) -> Array[Array[Float]]
|
18
|
+
end
|
data/sig/rmagick.rbs
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Magick
|
2
|
+
class Enum
|
3
|
+
end
|
4
|
+
class FilterType < Enum
|
5
|
+
end
|
6
|
+
PointFilter: FilterType
|
7
|
+
class ColorspaceType < Enum
|
8
|
+
end
|
9
|
+
GRAYColorspace: ColorspaceType
|
10
|
+
class DitherMethod < Enum
|
11
|
+
end
|
12
|
+
NoDitherMethod: DitherMethod
|
13
|
+
class Image
|
14
|
+
def quantize: (?Numeric, ?Magick::ColorspaceType, ?Magick::DitherMethod, ?Integer, ?bool) -> Magick::Image
|
15
|
+
def resize!: ((Float | Integer), ?(Float | Integer), ?Magick::FilterType, ?Float) -> Magick::Image
|
16
|
+
def export_pixels: (?Numeric, ?Numeric, ?Numeric, ?Numeric, ?String) -> Array[Numeric]
|
17
|
+
end
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dcthash
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Damien Kingsley
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-07-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rmagick
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.0'
|
27
|
+
description: This is a perceptual image hash calculation and comparison tool. This
|
28
|
+
can be used to match compressed or resized images to each other.
|
29
|
+
email:
|
30
|
+
- actuallydamo@gmail.com
|
31
|
+
executables: []
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- README.md
|
36
|
+
- lib/dcthash.rb
|
37
|
+
- lib/dcthash/version.rb
|
38
|
+
- sig/dcthash.rbs
|
39
|
+
- sig/rmagick.rbs
|
40
|
+
homepage: https://github.com/actuallydamo/dcthash
|
41
|
+
licenses:
|
42
|
+
- MIT
|
43
|
+
metadata:
|
44
|
+
homepage_uri: https://github.com/actuallydamo/dcthash
|
45
|
+
source_code_uri: https://github.com/actuallydamo/dcthash
|
46
|
+
rubygems_mfa_required: 'true'
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 2.7.0
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
requirements: []
|
62
|
+
rubygems_version: 3.3.7
|
63
|
+
signing_key:
|
64
|
+
specification_version: 4
|
65
|
+
summary: Generate and compare perceptual image hashes
|
66
|
+
test_files: []
|