dcthash 0.1.0
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/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: []
|