CLI tool to compare image similarities.
- Free software: GNU General Public License v3
Contents
Usage: image_compare [OPTIONS] INPUT_FILE OUTPUT_FILE A tool to compare given image pairs Options: --overwrite-output Overwrite the output if already exists [default: False] --quiet Suppress console output [default: False] --distance [ssim|nrmse|dhash|avghash|phash|whash] Similarity method to compare image pairs [default: ssim] --log-level [DEBUG|INFO|WARNING|ERROR|CRITICAL] Log level to control the output volume [default: INFO] --log-filename TEXT Log file path [default: image_compare.log] --help Show this message and exit.
Sample Commands:
# use default options image_compare files/product-cat-photos.csv files/product-cat-photos.csv # use dhash image_compare --distance=dhash files/product-cat-photos.csv files/product-cat-photos.csv # use whash and overwrite output image_compare --distance=whash --overwrite-output files/product-cat-photos.csv files/product-cat-photos.csv # use ssim and use my_log_file.log as logging, and no console output image_compare --distance=ssim --log-filename="my_log_file.log" --quiet \ files/product-cat-photos.csv files/product-cat-photos.csv
If you want to learn how to use image_compare programmatically please see the Usage Section
- Currently supports only CSV input/output formats
- Supports multiple comparision methods, namely;
- SSIM: Structural Similarity Index: https://en.wikipedia.org/wiki/Structural_similarity
- NRMSE: Normalized Root Mean Square Error: https://en.wikipedia.org/wiki/Root-mean-square_deviation#Normalized_root-mean-square_deviation
- DHash: Difference Hashing: http://www.hackerfactor.com/blog/index.php?/archives/529-Kind-of-Like-That.html
- AvgHash: Average Hashing: http://www.hackerfactor.com/blog/index.php?/archives/432-Looks-Like-It.html
- PHash: Perception Hashing: http://www.hackerfactor.com/blog/index.php?/archives/432-Looks-Like-It.html
- WHash: Wavelet Hashing: https://fullstackml.com/2016/07/02/wavelet-image-hash-in-python/
- The tool has following convenient features as well;
- Can log to file and console
- Can suppress console output, useful for automation
- Provides summary at the end of execution
- Descriptive error handling: in case of an error tool provides feedback to user about possible solution
- If an error occurs during the output file creation, the calculated distances will be written to log file.
Sample Files
image1 | image2 |
---|---|
files/images/cat.png | files/images/cat-box.png |
files/images/cat.png | files/images/cat-hue.png |
files/images/cat.png | files/images/cat-sampler.png |
files/images/cat.png | files/images/cat-wm-big.png |
files/images/cat.png | files/images/cat-wm-small.png |
image1 | image2 | similarity | elapsed |
---|---|---|---|
files/images/cat.png | files/images/cat-box.png | 0.016 | 1.421875 |
files/images/cat.png | files/images/cat-hue.png | 0.157 | 1.390625 |
files/images/cat.png | files/images/cat-sampler.png | 0.012 | 1.40625 |
files/images/cat.png | files/images/cat-wm-big.png | 0.014 | 1.375 |
files/images/cat.png | files/images/cat-wm-small.png | 0.005 | 1.390625 |
Following packages used for development and testing
Development
- Click==6.0
- scikit-image==0.15.0
- scipy==1.3.1
- imagehash==0.4
Testing & Building
- pip==19.2.2
- bump2version==0.5.10
- wheel==0.33.4
- watchdog==0.9.0
- flake8==3.7.8
- tox==3.13.2
- coverage==4.5.4 -> Test Coverage
- Sphinx==2.1.2 -> Automated documentation generation
- twine==1.13.0
- bandit==1.6.2 -> Static security analyzer
Due to technical problems the Coveralls.io integration is not working properly, but the html reports are stored under coverage_html_report folder. Also, you can click here to see the report online.
(Will update this section, when solving the integration issue)
To generate test coverage on your local installation run:
coverage run setup.py test coverage html
Modules
Here is brief description of each module and their components. Also, you can find more info at Module Index
- cli
- Contains Command Line Interface(CLI) definition and help text. This module parses user's input and creates a models.Config object to pass it to image_compare.main(config) method. This module contains the entry point of the project.
- exception
- Contains following custom exception classes, for error handling. * ICError(Exception): Base exception class * FileError(ICError): Represents file related errors * ArgumentError(ICError): Represents argument related logic errors
- file_handlers
- Contains the classes for parsing and writing files as well as a factory class to object creation based on the input/output file extension. This module currently supports only CSV files * FileHandlerFactory: * CSVInputHandler: Deals with the CSV file parsing and creating FilePair objects * CSVOutputHandler: Writes given FilePair objects in to a CSV file.
- image_compare
- This module deals with logging, exception handling and program flow.
- models
- Contains FilePair and Config data objects.
- similarity
- Contains the similarity calculation methods as well as the timing and registration functionality. Please see the Adding a new similarity measurement section for implementation details Supported methods are : SSIM, NRMSE, DHash, AHash, WHash, PHash. Please see Method Section for details.
- util
- Contains utility functions
Assuming you want to add a new commandline argument, namely log-filename.
Update your CLI definition in image_compare.cli module. Decorate image_compare.cli.main()
@click.option("--log-filename", default="image_compare.log",help="Log file path")
You must add new log_filename argument to main() method, updated main method signature should look like this
def main(input_file, output_file, overwrite_output, quiet, distance, log_level, log_filename):
Pass the new argument to Config object
config = Config(input_file, output_file, overwrite_output, quiet, distance, log_level, log_filename)
Update the image_compare.models.Config class and update test for initial values
Now you can use config.log_filename in image_compare.main() method
Final version of image_compare.cli.main method
@click.command() @click.argument("input_file") @click.argument("output_file") @click.option("--overwrite-output", is_flag=True, default=False, help="Overwrite the output if already exists") @click.option("--quiet", is_flag=True, default=False, help="Suppress console output") @click.option("--distance", type=click.Choice(get_supported_similarity_methods()), default="ssim", help="Similarity method to compare image pairs") @click.option("--log-level", type=click.Choice(image_compare.log_levels.keys()), default="INFO", help="Log level to control the output volume") @click.option("--log-filename", default="image_compare.log", help="Log file path") def main(input_file, output_file, overwrite_output, quiet, distance, log_level, log_filename): """A tool to compare given image pairs""" config = Config(input_file, output_file, overwrite_output, quiet, distance, log_level, log_filename) return image_compare.main(config)
Assuming you want to add a new similarity measurement algorithm called Structural Similarity Index Measure and there is already a python implementation in the project Skimage
- Update our requirements
Add a new entry to requirements_dev.txt file
skimage==0.15.0
Download dependencies
python -m pip install -r requirements_dev.txt
or
python -m pip install skimage==0.15.0
- Open image_compare/similarity.py file and add our new method
There are no constraints on the method name but the argument must be a FilePair object
After implementing the body in image_compare.similarity module, you should use @register(name="ssim") and @TimeSimilarityCalculation decorators.
- @register_distance(name)
decorator registers your new function as a similarity_measurement method and this method will be available with name's value e.g.`--distance=ssim` to CLI users without any more code update.
- @TimeSimilarityCalculation(timing_method=time.perf_counter)
decorator times the execution of the method and update the current image pairs pair.elapsed member. time.process_time is used as the default timing method.
- Process_time excludes time elapsed during sleep, if sleep time is important time.perf_counter() can be used.
See the documentation;
After completing calculation you must update pair.similarity.
Note: Simplified version showed below for demonstration purposes. You can check the full code at image_compare/similarity.py
@register_distance(name="ssim") @TimeSimilarityCalculation() def calculate_ssmi_similarity(pair): """Compute the mean structural similarity index between two images. :param pair: image pair to compare :return: """ image1, image2 = __check_files_and_open(pair) img1f = img_as_float(image1) img2f = img_as_float(image2) similarity = ssim(img1f, img2f, multichannel=True) pair.similarity = round(1 - similarity, 3)
Add some tests to tests/test_similarity.py and run them with python setup.py test
Install the updated version with python setup.py install and you can use your new method with;
image_compare --distance=ssim input.csv output.csv
- That is it. Your new function is ready to use! Please see section about releasing a new version section,
- if you want to publish your code changes to PyPI.
During development I created a small test data, which can be found under files/images folder
I applied some basic manipulation to create variations of the initial images. Image names contain the manipulation applied on it. Details
- Box: Added a solid box 1/16 of the image size
- Hue: Maxed out Hue slider on Photoshop
- CloneStamp: Manipulated images with Clone Stamp tool in Photoshop
- WM-size: Watermarked image in two sizes as big and small
- Crop-Left|Right Cropped %10 of the original image from Left or Right
Here are some sample images:
Credits for images
- Toronto Cityscape Photo by Alex Shutin on Unsplash
- Cat Photo by Yerlin Matu on Unsplash
- Nature1 Photo by eberhard grossgasteiger on Unsplash
- Nature2 Photo by Daniel Roe on Unsplash
This package was created with Cookiecutter and the audreyr/cookiecutter-pypackage project template.