Skip to content

Commit

Permalink
Merge pull request #7 from bunchesofdonald/richer
Browse files Browse the repository at this point in the history
Expose richer API for dealing directly with hashes
  • Loading branch information
bunchesofdonald committed Jan 25, 2016
2 parents ff22420 + 93d01c8 commit d461617
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 29 deletions.
22 changes: 19 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,23 @@ the comparison should be.::
import photohash
similar = photohash.is_look_alike('/path/to/myimage.jpg', '/path/to/myimage.jpg', tolerance=3)

hash_distance
-------------
Returns the hamming distance between two hashes of the same length::

import photohash
hash_one = average_hash('/path/to/myimage.jpg')
hash_two = average_hash('/path/to/myotherimage.jpg')
distance = photohash.hash_distance(hash_one, hash_two)

hashes_are_similar
------------------
Returns a boolean of whether or not the two hashes are within the given tolerance. Same as
is_look_alike, but takes hashes instead of image paths::

import photohash
hash_one = average_hash('/path/to/myimage.jpg')
hash_two = average_hash('/path/to/myotherimage.jpg')
similar = photohash.hash_are_similar(hash_one, hash_two)

TODO
====
* Add more hash algorithms.
hashes_are_similar also takes the same optional tolerance argument that is_look_alike does.
4 changes: 2 additions & 2 deletions photohash/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .photohash import average_hash, distance, is_look_alike
from .photohash import average_hash, hash_distance, is_look_alike, hashes_are_similar

__version__ = '0.3.2'
__version__ = '0.4.0'
30 changes: 18 additions & 12 deletions photohash/photohash.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,24 @@
from PIL import Image


def _hamming_distance(string, other_string):
""" Computes the hamming distance between two strings. """
if len(string) != len(other_string):
def hash_distance(left_hash, right_hash):
"""Compute the hamming distance between two hashes"""
if len(left_hash) != len(right_hash):
raise ValueError('Hamming distance requires two strings of equal length')

return sum(map(lambda x: 0 if x[0] == x[1] else 1, zip(string, other_string)))
return sum(map(lambda x: 0 if x[0] == x[1] else 1, zip(left_hash, right_hash)))


def hashes_are_similar(left_hash, right_hash, tolerance=6):
"""
Return True if the hamming distance between
the image hashes are less than the given tolerance.
"""
return hash_distance(left_hash, right_hash) <= tolerance


def average_hash(image_path, hash_size=8):
""" Computes the average hash of the given image. """
""" Compute the average hash of the given image. """
with open(image_path, 'rb') as f:
# Open the image, resize it and convert it to black & white.
image = Image.open(f).resize((hash_size, hash_size), Image.ANTIALIAS).convert('L')
Expand All @@ -27,17 +35,15 @@ def average_hash(image_path, hash_size=8):


def distance(image_path, other_image_path):
""" Computes the hamming distance between two images. """
""" Compute the hamming distance between two images"""
image_hash = average_hash(image_path)
other_image_hash = average_hash(other_image_path)

return _hamming_distance(image_hash, other_image_hash)
return hash_distance(image_hash, other_image_hash)


def is_look_alike(image_path, other_image_path, tolerance=6):
"""
Returns True if the hamming distance between
the image hashes are less than the given tolerance.
"""
image_hash = average_hash(image_path)
other_image_hash = average_hash(other_image_path)

return distance(image_path, other_image_path) <= tolerance
return hashes_are_similar(image_hash, other_image_hash, tolerance)
22 changes: 11 additions & 11 deletions photohash/tests/test_photohash.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from os.path import abspath, dirname, join
import unittest

from photohash.photohash import average_hash, distance, is_look_alike, hash_distance

TESTS_ROOT = join(dirname(abspath(__file__)))
ASSETS_ROOT = join(TESTS_ROOT, 'assets')
MODULE_ROOT = join(TESTS_ROOT, '../')

from photohash.photohash import average_hash, distance, is_look_alike, _hamming_distance


class PhotoHashTestCase(unittest.TestCase):
def setUp(self):
Expand Down Expand Up @@ -35,21 +35,21 @@ def test_average_hash(self):
for photo in self.photos:
self.assertEqual(photo['average_hash'], average_hash(photo['path']))

def test_hamming_distance(self):
"""_hamming_distance should know the distance between two strings"""
self.assertEqual(_hamming_distance('roses', 'toned'), 3)
self.assertEqual(_hamming_distance('are', 'are'), 0)
self.assertEqual(_hamming_distance('read', 'daer'), 4)
def test_hash_distance(self):
"""hash_distance should know the hamming distance between two strings"""
self.assertEqual(hash_distance('roses', 'toned'), 3)
self.assertEqual(hash_distance('are', 'are'), 0)
self.assertEqual(hash_distance('read', 'daer'), 4)

def test_hamming_distance_same_length_required(self):
"""_hamming_distance should throw a ValueError if the two strings are not the same length"""
self.assertRaises(ValueError, _hamming_distance, 'short', 'very long')
def testhash_distance_same_length_required(self):
"""hash_distance should throw a ValueError if the two strings are not the same length"""
self.assertRaises(ValueError, hash_distance, 'short', 'very long')

def test_distance(self):
"""distance should know the distance between the average_hash of two test images"""
for i in range(len(self.photos)):
for j in range(i, len(self.photos)):
hamming_distance = _hamming_distance(
hamming_distance = hash_distance(
self.photos[i]['average_hash'],
self.photos[j]['average_hash']
)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
name='Photohash',
packages=find_packages(),
url='https://github.com/bunchesofdonald/photohash',
version='0.3.2',
version='0.4.0',
install_requires=[
'Pillow>=2.1.0',
],
Expand Down

0 comments on commit d461617

Please sign in to comment.