diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..46c7830 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +language: python +python: + - "2.7" + - "3.4" + - "3.5" + - "3.6" + - "3.7" + - "3.8" + +install: + - pip install -e .[test] + - pip install coveralls +script: coverage run --source blockhash -m nose -v +after_success: + - coveralls diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..5bbfa6e --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include blockhash/resources/* +include blockhash/resources/scripts/* + diff --git a/README.md b/README.md deleted file mode 100644 index a2bb107..0000000 --- a/README.md +++ /dev/null @@ -1,23 +0,0 @@ -blockhash-python -================ - -This is a perceptual image hash calculation tool based on algorithm descibed in -Block Mean Value Based Image Perceptual Hashing by Bian Yang, Fan Gu and Xiamu Niu. - -Usage ------ - -This script requires Python 2.x or Python 3 and Python Imaging (PIL) 1.1.6 or above. - -Run `blockhash.py [list of images]` for calculating hashes. - -Run `blockhash.py --help` for the list of options. - -License -------- - -Copyright 2014 Commons Machinery http://commonsmachinery.se/ - -Distributed under an MIT license, please see LICENSE in the top dir. - -Contact: dev@commonsmachinery.se diff --git a/README.md b/README.md new file mode 120000 index 0000000..2391b39 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +blockhash/resources/README.md \ No newline at end of file diff --git a/assets/swiss_army_knife.jpg b/assets/swiss_army_knife.jpg new file mode 100644 index 0000000..5121a46 Binary files /dev/null and b/assets/swiss_army_knife.jpg differ diff --git a/blockhash/__init__.py b/blockhash/__init__.py new file mode 100644 index 0000000..7525d19 --- /dev/null +++ b/blockhash/__init__.py @@ -0,0 +1 @@ +__version__ = '0.1.4' diff --git a/blockhash/constants.py b/blockhash/constants.py new file mode 100644 index 0000000..ba2439d --- /dev/null +++ b/blockhash/constants.py @@ -0,0 +1 @@ +DEFAULT_BITS = 16 diff --git a/blockhash.py b/blockhash/core.py old mode 100755 new mode 100644 similarity index 69% rename from blockhash.py rename to blockhash/core.py index a2b0043..6f491c2 --- a/blockhash.py +++ b/blockhash/core.py @@ -1,14 +1,9 @@ -#! /usr/bin/env python -# -# Perceptual image hash calculation tool based on algorithm descibed in -# Block Mean Value Based Image Perceptual Hashing by Bian Yang, Fan Gu and Xiamu Niu -# # Copyright 2014 Commons Machinery http://commonsmachinery.se/ # Distributed under an MIT license, please see LICENSE in the top dir. import math -import argparse -import PIL.Image as Image + +import blockhash.constants def median(data): data = sorted(data) @@ -51,7 +46,7 @@ def bits_to_hexhash(bits): return '{0:0={width}x}'.format(int(''.join([str(x) for x in bits]), 2), width = len(bits) // 4) -def blockhash_even(im, bits): +def blockhash_even(im, bits=blockhash.constants.DEFAULT_BITS): if im.mode == 'RGBA': total_value = total_value_rgba elif im.mode == 'RGB': @@ -81,7 +76,7 @@ def blockhash_even(im, bits): translate_blocks_to_bits(result, blocksize_x * blocksize_y) return bits_to_hexhash(result) -def blockhash(im, bits): +def blockhash(im, bits=blockhash.constants.DEFAULT_BITS): if im.mode == 'RGBA': total_value = total_value_rgba elif im.mode == 'RGB': @@ -151,59 +146,3 @@ def blockhash(im, bits): translate_blocks_to_bits(result, block_width * block_height) return bits_to_hexhash(result) - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - - parser.add_argument('--quick', type=bool, default=False, - help='Use quick hashing method. Default: False') - parser.add_argument('--bits', type=int, default=16, - help='Create hash of size N^2 bits. Default: 16') - parser.add_argument('--size', - help='Resize image to specified size before hashing, e.g. 256x256') - parser.add_argument('--interpolation', type=int, default=1, choices=[1, 2, 3, 4], - help='Interpolation method: 1 - nearest neightbor, 2 - bilinear, 3 - bicubic, 4 - antialias. Default: 1') - parser.add_argument('--debug', action='store_true', - help='Print hashes as 2D maps (for debugging)') - parser.add_argument('filenames', nargs='+') - - args = parser.parse_args() - - if args.interpolation == 1: - interpolation = Image.NEAREST - elif args.interpolation == 2: - interpolation = Image.BILINEAR - elif args.interpolation == 3: - interpolation = Image.BICUBIC - elif args.interpolation == 4: - interpolation = Image.ANTIALIAS - - if args.quick: - method = blockhash_even - else: - method = blockhash - - for fn in args.filenames: - im = Image.open(fn) - - # convert indexed/grayscale images to RGB - if im.mode == '1' or im.mode == 'L' or im.mode == 'P': - im = im.convert('RGB') - elif im.mode == 'LA': - im = im.convert('RGBA') - - if args.size: - size = args.size.split('x') - size = (int(size[0]), int(size[1])) - im = im.resize(size, interpolation) - - hash = method(im, args.bits) - - print('{hash} {fn}'.format(fn=fn, hash=hash)) - - if args.debug: - bin_hash = '{:0{width}b}'.format(int(hash, 16), width=args.bits ** 2) - map = [bin_hash[i:i+args.bits] for i in range(0, len(bin_hash), args.bits)] - print("") - print("\n".join(map)) - print("") diff --git a/blockhash/resources/README.md b/blockhash/resources/README.md new file mode 100644 index 0000000..292e62f --- /dev/null +++ b/blockhash/resources/README.md @@ -0,0 +1,34 @@ +**This is a fork of the original, stale project to fix some of the issues, add CI verification, and push to PyPI.** + +[![Build Status](https://travis-ci.com/dsoprea/blockhash-python.svg?branch=master)](https://travis-ci.com/dsoprea/blockhash-python) +[![Coverage Status](https://coveralls.io/repos/github/dsoprea/blockhash-python/badge.svg?branch=master)](https://coveralls.io/github/dsoprea/blockhash-python?branch=master) + +blockhash-python +================ + +This is a perceptual image hash calculation tool based on algorithm descibed in +Block Mean Value Based Image Perceptual Hashing by Bian Yang, Fan Gu and Xiamu Niu. + +Installation +------------ + +Either download/clone the project directly, or install the "phash-blockhashio" package from PyPI. + + +Usage +----- + +This script requires Python 2.x or Python 3 and Python Imaging (PIL) 1.1.6 or above. + +Run `blockhash [list of images]` for calculating hashes. + +Run `blockhash --help` for the list of options. + +License +------- + +Copyright 2014 Commons Machinery http://commonsmachinery.se/ + +Distributed under an MIT license, please see LICENSE in the top dir. + +Contact: dev@commonsmachinery.se diff --git a/blockhash/resources/requirements-testing.txt b/blockhash/resources/requirements-testing.txt new file mode 100644 index 0000000..cafd3e0 --- /dev/null +++ b/blockhash/resources/requirements-testing.txt @@ -0,0 +1 @@ +nose2 diff --git a/blockhash/resources/requirements.txt b/blockhash/resources/requirements.txt new file mode 100644 index 0000000..3868fb1 --- /dev/null +++ b/blockhash/resources/requirements.txt @@ -0,0 +1 @@ +pillow diff --git a/blockhash/resources/scripts/blockhash b/blockhash/resources/scripts/blockhash new file mode 100755 index 0000000..e309020 --- /dev/null +++ b/blockhash/resources/scripts/blockhash @@ -0,0 +1,92 @@ +#! /usr/bin/env python +# +# Perceptual image hash calculation tool based on algorithm descibed in +# Block Mean Value Based Image Perceptual Hashing by Bian Yang, Fan Gu and Xiamu Niu +# +# Copyright 2014 Commons Machinery http://commonsmachinery.se/ +# Distributed under an MIT license, please see LICENSE in the top dir. + +import sys +import os +_APP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) +sys.path.insert(0, _APP_PATH) + +import argparse + +import PIL.Image as Image + +import blockhash.constants +import blockhash.core +import blockhash.utility + +def _main(): + parser = argparse.ArgumentParser() + + parser.add_argument( + '--quick', + action='store_true', + help='Use quick hashing method. Default: False') + + parser.add_argument( + '--bits', + type=int, default=blockhash.constants.DEFAULT_BITS, + help='Create hash of size N^2 bits.') + + parser.add_argument( + '--size', + help='Resize image to specified size before hashing, e.g. 256x256') + + parser.add_argument( + '--interpolation', + type=int, + default=1, + choices=[1, 2, 3, 4], + help='Interpolation method: 1 - nearest neightbor, 2 - bilinear, 3 - bicubic, 4 - antialias. Default: 1') + + parser.add_argument( + '--debug', + action='store_true', + help='Print hashes as 2D maps (for debugging)') + + parser.add_argument( + 'filenames', + nargs='+') + + args = parser.parse_args() + + if args.interpolation == 1: + interpolation = Image.NEAREST + elif args.interpolation == 2: + interpolation = Image.BILINEAR + elif args.interpolation == 3: + interpolation = Image.BICUBIC + elif args.interpolation == 4: + interpolation = Image.ANTIALIAS + + if args.quick: + method = blockhash.core.blockhash_even + else: + method = blockhash.core.blockhash + + for fn in args.filenames: + with Image.open(fn) as im: + im = blockhash.utility.normalize_image(im) + + if args.size: + size = args.size.split('x') + size = (int(size[0]), int(size[1])) + im = im.resize(size, interpolation) + + hash_ = method(im, args.bits) + + print('{hash} {fn}'.format(fn=fn, hash=hash_)) + + if args.debug: + bin_hash = '{:0{width}b}'.format(int(hash_, 16), width=args.bits ** 2) + map = [bin_hash[i:i+args.bits] for i in range(0, len(bin_hash), args.bits)] + print("") + print("\n".join(map)) + print("") + +if __name__ == '__main__': + _main() diff --git a/blockhash/utility.py b/blockhash/utility.py new file mode 100644 index 0000000..0032572 --- /dev/null +++ b/blockhash/utility.py @@ -0,0 +1,8 @@ +def normalize_image(im): + # convert indexed/grayscale images to RGB + if im.mode == '1' or im.mode == 'L' or im.mode == 'P': + return im.convert('RGB') + elif im.mode == 'LA': + return im.convert('RGBA') + + return im diff --git a/requirements-testing.txt b/requirements-testing.txt new file mode 120000 index 0000000..3a1e258 --- /dev/null +++ b/requirements-testing.txt @@ -0,0 +1 @@ +blockhash/resources/requirements-testing.txt \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 120000 index 0000000..1fb57de --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +blockhash/resources/requirements.txt \ No newline at end of file diff --git a/setup.py b/setup.py index 89a1c4f..e331b2c 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,43 @@ #!/usr/bin/env python -from distutils.core import setup +import os +import setuptools -setup( - name='blockhash', - version='0.1', +import blockhash + +_APP_PATH = os.path.dirname(blockhash.__file__) + +with open(os.path.join(_APP_PATH, 'resources', 'README.md')) as f: + long_description = f.read() + +with open(os.path.join(_APP_PATH, 'resources', 'requirements.txt')) as f: + install_requires = [s.strip() for s in f.readlines()] + +with open(os.path.join(_APP_PATH, 'resources', 'requirements-testing.txt')) as f: + test_requires = [s.strip() for s in f.readlines()] + +setuptools.setup( + name='phash-blockhashio', + version=blockhash.__version__, description='Perceptual image hash calculation tool', + long_description=long_description, + long_description_content_type='text/markdown', author='Commons Machinery', author_email='dev@commonsmachinery.se', license='MIT', - scripts=['blockhash.py'], - requires=['pillow'], + scripts=[ + 'blockhash/resources/scripts/blockhash' + ], + install_requires=install_requires, + tests_require=install_requires + test_requires, + url='https://github.com/dsoprea/blockhash-python', + packages=setuptools.find_packages(exclude=['tests']), + include_package_data=True, + package_data={ + 'blockhash': [ + 'resources/README.md', + 'resources/requirements.txt', + 'resources/requirements-testing.txt', + ], + }, ) diff --git a/test/command/__init__.py b/test/command/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/command/test_blockhash.py b/test/command/test_blockhash.py new file mode 100644 index 0000000..d679b96 --- /dev/null +++ b/test/command/test_blockhash.py @@ -0,0 +1,46 @@ +import unittest +import os +import subprocess + +_APP_PATH = os.path.join(os.path.dirname(__file__), '..', '..') +_ASSETS_PATH = os.path.join(_APP_PATH, 'assets') +_SCRIPT_FILEPATH = os.path.join(_APP_PATH, 'blockhash', 'resources', 'scripts', 'blockhash') + + +class TestCommand(unittest.TestCase): + def test_run__slow(self): + filepath = os.path.join(_ASSETS_PATH, 'swiss_army_knife.jpg') + + cmd = [ + _SCRIPT_FILEPATH, + filepath, + ] + + output = \ + subprocess.check_output( + cmd, + stderr=subprocess.STDOUT, + universal_newlines=True) + + parts = output.split() + + self.assertEquals(parts[0], 'ff4ff907f801e800ff01fc83fc03f003e00fc20fe00ff1cff00ff81ffc1ffe7f') + + def test_run__quick(self): + filepath = os.path.join(_ASSETS_PATH, 'swiss_army_knife.jpg') + + cmd = [ + _SCRIPT_FILEPATH, + '--quick', + filepath, + ] + + output = \ + subprocess.check_output( + cmd, + stderr=subprocess.STDOUT, + universal_newlines=True) + + parts = output.split() + + self.assertEquals(parts[0], 'ffcff903f801e800ff01fc03fc03f803e00fc20fe00ff1cff00ff01ffc1ffe7f') diff --git a/test/test_blockhash.py b/test/test_blockhash.py index 3be5056..210c63a 100644 --- a/test/test_blockhash.py +++ b/test/test_blockhash.py @@ -5,50 +5,74 @@ # Distributed under an MIT license, please see LICENSE in the top dir. import unittest -import blockhash +import os +import glob + import PIL.Image as Image -import os, glob - -datadir = os.path.join(os.path.dirname(__file__), 'data') - -class BlockhashTestCase(unittest.TestCase): - def __init__(self, img_filename=None, hash_filename=None, method=None, bits=None): - unittest.TestCase.__init__(self) - self.img_filename = img_filename - self.hash_filename = hash_filename - self.method = method - self.bits = bits - - def runTest(self): - im = Image.open(self.img_filename) - - # convert indexed/grayscale images to RGB - if im.mode == '1' or im.mode == 'L' or im.mode == 'P': - im = im.convert('RGB') - elif im.mode == 'LA': - im = im.convert('RGBA') - - with open(self.hash_filename) as f: - expected_hash = f.readline().split()[0] - - if self.method == 1: - method = blockhash.blockhash_even - elif self.method == 2: - method = blockhash.blockhash - - hash = method(im, self.bits) - hash = "".join([str(x) for x in hash]) - self.assertEqual(expected_hash, hash) - -def load_tests(loader, tests, pattern): - test_cases = unittest.TestSuite() - for img_fn in (glob.glob(os.path.join(datadir, '*.jpg')) + - glob.glob(os.path.join(datadir, '*.png'))): - for m in range(2): - bits = 16 - method = m + 1 - basename, ext = os.path.splitext(img_fn) - hash_fn = basename + '_{}_{}.txt'.format(bits, method) - test_cases.addTest(BlockhashTestCase(img_fn, hash_fn, method, bits)) - pass - return test_cases + +import blockhash.core + +_APP_PATH = os.path.join(os.path.dirname(__file__), '..') +_ASSETS_PATH = os.path.join(_APP_PATH, 'assets') + +# datadir = os.path.join(os.path.dirname(__file__), 'data') + +# class BlockhashTestCase(unittest.TestCase): +# def __init__(self, img_filename=None, hash_filename=None, method=None, bits=None): +# unittest.TestCase.__init__(self) +# self.img_filename = img_filename +# self.hash_filename = hash_filename +# self.method = method +# self.bits = bits + +# def runTest(self): +# im = Image.open(self.img_filename) + +# # convert indexed/grayscale images to RGB +# if im.mode == '1' or im.mode == 'L' or im.mode == 'P': +# im = im.convert('RGB') +# elif im.mode == 'LA': +# im = im.convert('RGBA') + +# with open(self.hash_filename) as f: +# expected_hash = f.readline().split()[0] + +# if self.method == 1: +# method = blockhash.core.blockhash_even +# elif self.method == 2: +# method = blockhash.core.blockhash + +# hash = method(im, self.bits) +# hash = "".join([str(x) for x in hash]) +# self.assertEqual(expected_hash, hash) + +# def load_tests(loader, tests, pattern): +# test_cases = unittest.TestSuite() +# for img_fn in (glob.glob(os.path.join(datadir, '*.jpg')) + +# glob.glob(os.path.join(datadir, '*.png'))): +# for m in range(2): +# bits = 16 +# method = m + 1 +# basename, ext = os.path.splitext(img_fn) +# hash_fn = basename + '_{}_{}.txt'.format(bits, method) +# test_cases.addTest(BlockhashTestCase(img_fn, hash_fn, method, bits)) +# pass +# return test_cases + + +class TestBlockhash(unittest.TestCase): + def test_blockhash_even(self): + filepath = os.path.join(_ASSETS_PATH, 'swiss_army_knife.jpg') + + with Image.open(filepath) as im: + digest = blockhash.core.blockhash_even(im, 16) + + self.assertEquals(digest, 'ffcff903f801e800ff01fc03fc03f803e00fc20fe00ff1cff00ff01ffc1ffe7f') + + def test_blockhash(self): + filepath = os.path.join(_ASSETS_PATH, 'swiss_army_knife.jpg') + + with Image.open(filepath) as im: + digest = blockhash.core.blockhash(im, 16) + + self.assertEquals(digest, 'ff4ff907f801e800ff01fc83fc03f003e00fc20fe00ff1cff00ff81ffc1ffe7f')