"""
Introduction
=============
The `dhashpy <https://github.com/akamhy/dhashpy>`_ implements the row-wise
gradient dHash (difference hash) algorithm.
dHash algorithm was originally described at `Kind of Like That - The Hacker Factor Blog
<https://www.hackerfactor.com/blog/index.php?/archives/529-Kind-of-Like-That.html>`_
The dHash algorithm works by converting the input image to grayscale,
shrinking it to a smaller size and then leveraging the fact that for the
grayscale images, the pixel value is just a single number that represents
the brightness of the pixel. And measuring the difference in brightness
between the adjacent pixels to generate the hash value for the input image.
The difference between the brightness of the grayscaled image can be
calculated either row-wise(along the row) or column-wise(along the columns).
This identifies the relative brightness gradient direction for the rows or
the columns respectively.
The dhashpy package implements the row-wise difference based dHash algorithm
and therefore calculates difference between in brightness the adjacent
pixels in a row and not in a column.
The dHash algorithm in dhashpy
==============================
The exact implementation of the row-wise difference dHash algorithm in
dhashpy package is explained below. And can be used to calculate the exact
same hash output if you want to implement it in an another language.
- Convert the input image to grayscale using the ITU-R 601-2 luma transform.
**L = R * 299/1000 + G * 587/1000 + B * 114/1000**
Every pixel is now just an integer, a shade of grey from the possible 256
shades of grey.
- Remove the high frequencies and detail by shrinking(resize) the image.
The default setting is to resize every image to 9x8 pixels(width, height).
As this implementation takes row wise difference, we need to shrink the
image to **(n+1)x(n)**. The width is bigger by one pixel. There are now
total (n^2 + n) pixels/shade of grey in the image.
- Now compare the adjacent pixels along a row. And if the pixel on the right
side of the current pixel is brighter than the current pixel then set the bit
value to 1 else set the value to 0. As there are (n+1) pixel is a row thus
there will be n number of differences. Total number of differences for all
the n rows will be (n^2)bits.
- Join the values from left to right and top to bottom to form a binary
string. Prefix the string with "0b", indicating that it's
a bitstring. The length of this binary string should be (n^2 + 2),
(n^2) number of bit and 2 for the prefix "0b".
- As the string is a binary string and too long, it's better to store the
hexadecimal value of this binary string if storage is an issue.
But if storing large strings is not an issue it's better to store the
binary string as storing the binary strings will reduce the compute cost
of converting the hexadecimal strings to binary every time we want to
compare the hash values. The comparisons must be done on the original
binary string and never on the hexadecimal representation of the strings.
Hexadecimal strings should be prefixed with "0x" indicating that they are
hexadecimal representation of the hash value.
- Hamming distance is used to calculate the difference between the hash
values(the binary strings). The result of the hamming distance of the
two binary strings is the relative difference between the two images.
Hamming distance for strings of unequal length is not defined.
Example, when n=8, the default value:
=====================================
- If n = 8, height is 8 pixels. The width(n+1) should be 8+1 = 9 pixels.
The following structure is a 9x8 pixel distribution::
|----------------- WIDTH ----------------------->
H px1 px2 px3 px4 px5 px6 px7 px8 px9
E px10 px11 px12 px13 px14 px15 px16 px17 px18
I px19 px20 px21 px22 px23 px24 px25 px26 px27
G px28 px29 px30 px31 px32 px33 px34 px35 px36
H px37 px38 px39 px40 px41 px42 px43 px44 px45
T px46 px47 px48 px49 px50 px51 px52 px53 px54
| px55 px56 px57 px58 px59 px60 px61 px62 px63
| px64 px65 px66 px67 px68 px69 px70 px71 px72
V
- For pixels in a same row let's define a function, value(x) such that:
**value(x) = 0 for px(x+1) > px(x)**
**value(x) = 1 for px(x+1) < px(x)**
.. note::
We can change the definition of the above function and set value(x) = 1 if
px(x+1) > px(x) and vice-versa, it's arbitrary but it's very important to be
consistent.
- If the next pixel value in the same row is a bigger(brighter) number than the current
pixel set the value of pixel's position to 1 else 0. For the last
pixel of each row we don't have the value(x) defined and therefore there
are n values for (n+1) pixels.
- For total n number of rows we get (n^2) pixels. Here the number of
rows is 8 and columns is 9, which is 72 (8*9) pixels and
64 (8*8) pixel difference values.
- If px(46+1) > px(46) == px(47) > px(46) then we set the value(46) to 0
- If px(47) < px(46) than set the value(x) = 1
- But value(27) is not defined as there is no pixel in the same row to
compare with.
- Last pixel of every row is non conformable to the difinition of value(x)
as last pixels do not have any next pixel in the same row.
API reference
=============
"""
from PIL import Image
import os.path
[docs]class DHash(object):
"""
DHash class
=============
DHash class provides an interface for computing & comparing dhash values.
DHash class and it's instance have the following public methods:
- DHash.hamming_distance(str_a, str_b) : It takes two strings as input for
which the hamming distance will be
returned.
- DHash.hex2bin(hexstr, padding) : hexstr is the hexadecimal string for
which we want to calculate the binary
value. The binary value is returned as a
string prefixed with "0b", indicating
that the string is binary value.
padding is an integer and is useful when
we compute the hamming distance of the
output. Hamming distance is not defined
for strings of unequal length.
- DHash.bin2hex(binstr) : Input binary string is converted to output
hexadecimal value. Both the input binary value and
the output hexadecimal value should be prefixed
with "0b" and "0x" respectively.
DHash objects have the following attributes:
- DHash.path : The path of the input image.
- DHash.image : Instance of PIL.Image.Image class with the input as the
input image.
- DHash.hash : A binary string prefixed with "0b" is the hash of the input
image.
- DHash.hash_hex : Hexadecimal representation of the binary string hash.
- DHash.height : The resized height of the input image. Units are pixels.
- DHash.width : The resized width of the input image. Units are pixels
- DHash.bits_in_hash : Total number of bits in the hash. Equal n^2, where n
is the height.
"""
def __init__(self, path: str, height: int = 8) -> None:
"""
Check if path exists.
:param path: absolute path of the image that needs to be hashed.
:param height: height must be an integer, it's the height of the scaled
image. Units are pixels.
If you set height to 4, you will get a
4^2 = 16 bits hash value.
If you set height to 9 you will get a 81(9^2=81) bits hash.
The default value is 8, the default number of bits is 64.
The binary string is prefixed with "0b", so a 64 bits hash
will have a length of 66(64 hash + 2 for 0b), a 16 bits
hash will have a length of 18(16 bit hash and 2 for 0b prefix).
:return: None
:rtype: NoneType
"""
self.path = path
self.height = height
self.bits_in_hash = self.height * self.height
self.width = self.height + 1
if not os.path.isfile(self.path):
raise FileNotFoundError("No image file found at '%s'." % self.path)
self._calc_hash()
def __str__(self) -> str:
"""
String representation of the instance of DHash class and is same as DHash.hash
:return: Binary hash value for the image, prefixed with "0b".
:rtype: str
"""
return self.hash
def __len__(self) -> int:
"""
length of the hash, including the prefix 0b. len = bits_in_hash + 2
:return: Length of the binary hash value for the image.
:rtype: int
"""
return len(self.hash)
def __repr__(self) -> str:
"""
Representation of the instance of DHash class.
:return: String representation of the object of DHash class.
:rtype: str
"""
return "DHash(hash=%s, hash_hex=%s, path=%s)" % (
self.hash,
self.hash_hex,
self.path,
)
def __ne__(self, other: object) -> bool:
"""
Implement '!=' on the instance of DHash class.
:param other: Can be instance of DHash class or a string starting
with "0x" or "0b", representing hexadecimal or binary
value.
:return: Return True if not equal else return False.
:rtype: bool
"""
if self.__eq__(other):
return False
return True
def __eq__(self, other: object) -> bool:
"""
Implement '==' on the instance of DHash class.
:param other: Can be instance of DHash class or a string starting
with "0x" or "0b", representing hexadecimal or binary
value.
:return: Return True if equal else return False.
:rtype: bool
"""
if self.__sub__(other) == 0:
return True
return False
def __sub__(self, other: object) -> int:
"""
Implement the usage of '-' on instance of DHash class and
Also compatibile with hexadecimal and binary strings
:param other: Can be instance of DHash class or a string starting
with "0x" or "0b", representing hexadecimal or binary
value. If binary string is supplied hash must of the same
bits as this instance.
:return: Hamming distance of the two objects/binary string/hexadecimal
string being compared.
:rtype: int
"""
if other is None:
raise TypeError("Other hash is None. And it must not be None.")
if isinstance(other, str):
if other.lower().startswith("0x"):
return DHash.hamming_distance(
self.hash, DHash.hex2bin(other.lower(), self.bits_in_hash)
)
elif other.lower().startswith("0b"):
if len(other) != len(self.hash):
raise ValueError(
"Can not compare different bits hashes. You must supply a %d bits hash."
% self.bits_in_hash
)
return DHash.hamming_distance(self.hash, other.lower())
else:
raise TypeError(
"Hash string must start with either '0x' for hexadecimal or '0b' for binary."
)
if isinstance(other, DHash):
return DHash.hamming_distance(self.hash, other.hash)
raise TypeError(
"To calculate difference both of the hashes must be either hexadecimal/binary strings or instance of DHash"
)
def _calc_hash(self) -> None:
"""
Open the input image using the pillow package.
Converts the image to greyscale.
Resize the image to a smaller pixel value.
Calculate the binary hash value with the DHash algorithm.
dHash algorithm as defined at :
`Kind of Like That - The Hacker Factor Blog
<https://www.hackerfactor.com/blog/index.php?/archives/529-Kind-of-Like-That.html>`_
:return: None
:rtype: NoneType
"""
self.image = Image.open(self.path)
self.image = self.image.convert("L")
self.image = self.image.resize((self.width, self.height), Image.ANTIALIAS)
lpixels = list(self.image.getdata())
self.hash = "0b"
for i, pixel in enumerate(lpixels):
if (i + 1) % self.width == 0 and i != 0:
continue
if pixel < lpixels[i + 1]:
self.hash += "1"
continue
self.hash += "0"
self.hash_hex = DHash.bin2hex(self.hash)
[docs] @staticmethod
def hamming_distance(string_a: str, string_b: str) -> int:
"""
Computes and returns the hamming distance between the
two input strings.
The two input strings must be of equal length as the Hamming distance is
undefined when strings are of unequal length.
:param string_a: A python string representing a binary number, prefixed with "0b"
:param string_b: A python string representing a binary number, prefixed with "0b"
:return: Hamming distance between the two input strings.
:rtype: int
:raises ValueError: When both the strings are not of equal length.
"""
if len(string_a) != len(string_b):
raise ValueError(
"Strings are of unequal length can not compute hamming distance. Hamming distance is undefined."
)
return sum(char_1 != char_2 for char_1, char_2 in zip(string_a, string_b))
[docs] @staticmethod
def hex2bin(hexstr: str, padding: int) -> str:
"""
Convert the input string from hexadecimal to binary representation.
:param hexstr: hexadecimal string and must be prefixed with "0x".
:param padding: integer indicating the required padding for the string.
padding is useful if hamming distance of the output
binary string is to be computed. Hamming distance is not
defined for strings of unequal length.
:return: Binary representation of the input hexadecimal string.
:rtype: str
:raises ValueError: If hexadecimal input string is not prefixed with "0x".
"""
if not hexstr.lower().startswith("0x"):
raise ValueError("Input hexadecimal string must have '0x' as the prefix.")
return "0b" + str(bin(int(hexstr.lower(), 0))).replace("0b", "").zfill(padding)
[docs] @staticmethod
def bin2hex(binstr: str) -> str:
"""
Converts input string from Binary to hexadecimal representation.
:param binstr: Binary string and must be prefixed with "0b".
:return: Hexadecimal representation of the input
binary string prefixed with "0x" indicating that it's
hexadecimal string.
:rtype: str
:raises ValueError: If binary input string is not prefixed with "0b".
"""
if not binstr.lower().startswith("0b"):
raise ValueError("Binary string must be prefixed with '0b'.")
return str(hex(int(binstr, 2)))