Title: Building an Image Hashing Search Engine with VP-Trees and OpenCV - PyImageSearch

URL Source: https://pyimagesearch.com/2019/08/26/building-an-image-hashing-search-engine-with-vp-trees-and-opencv/

Published Time: 2019-08-26T14:00:40+00:00

Markdown Content:
[![Image 1](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20600%20383'%3E%3C/svg%3E)](https://pyimagesearch.com/wp-content/uploads/2019/08/image_hashing_search_engine_header.png)

In this tutorial, you will learn how to build a scalable image hashing search engine using OpenCV, Python, and VP-Trees.

**Image hashing algorithms are used to:**

1.  Uniquely quantify the contents of an image using only a _single integer._
2.  Find _**duplicate**_ or _**near-duplicate images**_ in a dataset of images based on their computed hashes.

Back in 2017, I wrote a tutorial on [image hashing with OpenCV and Python](https://pyimagesearch.com/2017/11/27/image-hashing-opencv-python/) (which is _**required reading**_ for this tutorial). That guide showed you how to find _identical/duplicate_ images in a given dataset.

However, there was a scalability problem with that original tutorial — _**namely that it did not scale!**_

To find _near-duplicate_ images, our original image hashing method would require us to perform a _linear search_, comparing the query hash to each individual image hash in our dataset.

In a practical, real-world application that’s far too slow — we need to find a way to reduce that search to sub-linear time complexity.

But how can we reduce search time so dramatically?

The answer is a specialized data structure called a VP-Tree.

Using a VP-Tree we can reduce our search complexity from _O(n)_ to _O(log n)_, enabling us to obtain our sub-linear goal!

**In the remainder of this tutorial you will learn how to:**

1.  Build an image hashing search engine to find both _identical_ and _near-identical_ images in a dataset.
2.  Utilize a specialized data structure, called a VP-Tree, that can be used used to scale image hashing search engines to millions of images.

**To learn how to build your first image hashing search engine with OpenCV, _just keep reading!_**

![Image 2](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20128%20128'%3E%3C/svg%3E)

#### Looking for the source code to this post?

[Jump Right To The Downloads Section](#download-the-code)

In the first part of this tutorial, I’ll review what exactly an image search engine is for newcomers to PyImageSearch.

Then, we’ll discuss the concept of image hashing and perceptual hashing, including how they can be used to build an image search engine.

We’ll also take a look at problems associated with image hashing search engines, including algorithmic complexity.

_**Note:** If you haven’t read my tutorial on [Image Hashing with OpenCV and Python](https://pyimagesearch.com/2017/11/27/image-hashing-opencv-python/), **make sure you do so now**. That guide is **required reading** before you continue here._

From there, we’ll briefly review Vantage-point Trees (VP-Trees) which can be used to _dramatically_ improve the efficiency and performance of image hashing search engines.

Armed with our knowledge we’ll implement our own custom image hashing search engine using VP-Trees and then examine the results.

### What is an image search engine?

[![Image 3](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20600%20767'%3E%3C/svg%3E)](https://pyimagesearch.com/wp-content/uploads/2019/08/image_hashing_traditional_search_engine.png)

**Figure 1:** An example of an image search engine. A query image is presented and the search engine finds similar images in a dataset.

In this section, we’ll review the concept of an image search engine and direct you to some additional resources.

PyImageSearch has roots in image search engines — that was my main interest when I started the blog back in 2014. This tutorial is a fun one for me to share as I have a soft spot for image search engines as a computer vision topic.

Image search engines are a lot like textual search engines, only instead of using _text_ as a query, we instead use an _image._

When you use a **_text search engines_**, such as Google, Bing, or DuckDuckGo, etc., you enter your search _query_ — a word or phrase. Indexed websites of interest are returned to you as _results,_ and ideally, you’ll find what you are looking for.

Similarly, for an **_image search engine_**, you present a _query_ image (not a textual word/phrase). The image search engine then returns similar image results based **_solely on the contents of the image._**

Of course, there is a lot that goes on under the hood in any type of search engine — just keep this key concept of query/results in mind going forward as we build an image search engine today.

**To learn more about image search engines, I suggest you refer to the following resources:**

*   [_The complete guide to building an image search engine with Python and OpenCV_](https://pyimagesearch.com/2014/12/01/complete-guide-building-image-search-engine-python-opencv/)
    *   A great guide for those who want to get started with enough knowledge to be dangerous.
*   [Image Search Engines Blog Category](https://pyimagesearch.com/category/examples-of-image-search-engines/)
    *   This category link returns all of my image search engine content on the PyImageSearch blog.
*   [PyImageSearch Gurus](https://pyimagesearch.com/pyimagesearch-gurus/)
    *   My flagship computer vision course has 13 modules, one of which is dedicated to _Content-Based Image Retrieval_ (a fancy name for image search engines).

Read those guides to obtain a basic understanding of what an image search engine is, then come back to this post to learn about image hash search engines.

### What is image hashing/perceptual hashing?

[![Image 4](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20600%20433'%3E%3C/svg%3E)](https://pyimagesearch.com/wp-content/uploads/2019/08/image_hashing_search_engine_hash_example.png)

**Figure 2:** An example of an image hashing function. _Top-left:_ An input image. _Top-right:_ An image hashing function. _Bottom:_ The resulting hash value. We will build a basic image hashing search engine with VP-Trees and OpenCV in this tutorial.

**Image hashing**, also called **perceptual hashing**, is the process of:

1.  Examining the contents of an image.
2.  Constructing a hash value (i.e., an integer) that _uniquely quantifies_ an input image _based on the contents of the image alone_.

**One of the benefits of using image hashing is that the resulting storage used to quantify the image is super small.**

For example, let’s suppose we have an _800x600px_ image with 3 channels. If we were to store that entire image in memory using an 8-bit unsigned integer data type, the image would require 1.44MB of RAM.

Of course, we would rarely, if ever, store the raw image pixels when quantifying an image.

Instead, we would use algorithms such as keypoint detectors and local invariant descriptors (i.e., SIFT, SURF, etc.).

Applying these methods can typically lead to 100s to 1000s of features _per image_.

If we assume a modest 500 keypoints detected, each resulting in a feature vector of 128-d with a 32-bit floating point data type, we would require a total of 0.256MB to store the quantification of each individual image in our dataset.

**Image hashing, on the other hand, allows us to quantify an image using only a 32-bit integer, requiring only 4 bytes of memory!**

[![Image 5](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20600%20249'%3E%3C/svg%3E)](https://pyimagesearch.com/wp-content/uploads/2019/08/image_hashing_search_engine_sizes.png)

**Figure 3:** An image hash requires far less disk space in comparison to the original image bitmap size or image features (SIFT, etc.). We will use image hashes as a basis for an image search engine with VP-Trees and OpenCV.

Furthermore, **image hashes should also be _comparable_**.

Let’s suppose we compute image hashes for three input images, two of which near-identical images:

[![Image 6](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20600%20284'%3E%3C/svg%3E)](https://pyimagesearch.com/wp-content/uploads/2019/08/image_hashing_search_engine_hashes.png)

**Figure 4:** Three images with different hashes. The Hamming Distance between the top two hashes is closer than the Hamming distance to the third image. We will use a VP-Tree data structure to make an image hashing search engine.

**To compare our image hashes we will use the Hamming distance.** The Hamming distance, in this context, is used to compare the number of different bits between two integers.

**In practice, this means that we count the number of 1s when taking the XOR between two integers.**

Therefore, going back to our three input images above, the Hamming distance between our two similar images should be _smaller_ (indicating _more similarity_) than the Hamming distance between the third less similar image:

[![Image 7](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20600%20445'%3E%3C/svg%3E)](https://pyimagesearch.com/wp-content/uploads/2019/08/image_hashing_search_engine_distances.png)

**Figure 5:** The Hamming Distance between image hashes is shown. Take note that the Hamming Distance between the first two images is smaller than that of the first and third (or 2nd and 3rd). The Hamming Distance between image hashes will play a role in our image search engine using VP-Trees and OpenCV.

**Again, note how the Hamming distance between the two images is _smaller_ than the distances between the third image:**

*   The _smaller_ the Hamming distance is between two hashes, the _more similar_ the images are.
*   And conversely, the _larger_ the Hamming distance is between two hashes, the _less similar_ the images are.

Also note how the distance between identical images (i.e., along the diagonal of **Figure 5**) are all zero — the Hamming distance between two hashes will be zero if the two input images are _identical_, otherwise the distance will be > 0, with larger values indicating less similarity.

There are a number of image hashing algorithms, but one of the most popular ones is called the **difference hash**, which includes four steps:

1.  **Step #1:** Convert the input image to grayscale.
2.  **Step #2:** Resize the image to fixed dimensions, _N + 1 x N_, ignoring aspect ratio. Typically we set _N=8_ or _N=16_. We use _N + 1_ for the number of rows so that we can compute the difference (hence _“difference hash”_) between adjacent pixels in the image.
3.  **Step #3:** Compute the difference. If we set _N=8_ then we have 9 pixels per row and 8 pixels per column. We can then compute the difference between adjacent column pixels, yielding _8_ differences. _8_ rows of 8 differences (i.e., _8×8_) results in _64_ values.
4.  **Step #4:** Finally, we can build the hash. In practice all we actually need to perform is a “greater than” operation comparing the columns, yielding binary values. These _64_ binary values are compacted into an integer, forming our final hash.

**Typically, image hashing algorithms are used to find _near-duplicate images_ in a large dataset.**

**I’ve covered image hashing in detail [inside this tutorial](https://pyimagesearch.com/2017/11/27/image-hashing-opencv-python/)** so if the concept is new to you, I would suggest reading that guide before continuing here.

### What is an image hashing search engine?

[![Image 8](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20500%20688'%3E%3C/svg%3E)](https://pyimagesearch.com/wp-content/uploads/2019/08/image_hashing_search_engine_steps.png)

**Figure 6:** Image search engines consist of images, an indexer, and a searcher. We’ll index all of our images by computing and storing their hashes. We’ll build a VP-Tree of the hashes. The searcher will compute the hash of the query image and search the VP tree for similar images and return the closest matches. Using Python, OpenCV, and [vptree](https://github.com/RickardSjogren/vptree), we can implement our image hashing search engine.

An image hashing search engine consists of two components:

*   **Indexing:** Taking an input dataset of images, computing the hashes, and storing them in a data structure to facilitate fast, efficient search.
*   **Searching/Querying:** Accepting an input query image from the user, computing the hash, and finding all near-identical images in our indexed dataset.

A great example of an image hashing search engine is [TinEye](https://tineye.com/), which is actually a **reverse image search engine**.

A reverse image search engine:

1.  Accepts an input image.
2.  Finds all near-duplicates of that image on the web, telling you the website/URL of where the near duplicate can be found.

**Using this tutorial you will learn how to build your own TinEye!**

### What makes scaling image hashing search engines problematic?

One of the biggest issues with building an image hashing search engine is **scalability** — the more images you have, the longer it can take to perform the search.

For example, let’s suppose we have the following scenario:

*   We have a dataset of 1,000,000 images.
*   We have already computed image hashes for each of these 1,000,000 images.
*   A user comes along, presents us with an image, and then asks us to find all near-identical images in that dataset.

How might you go about performing that search?

Would you loop over all 1,000,000 image hashes, one by one, and compare them to the hash of the query image?

**Unfortunately, that’s not going to work.** Even if you assume that each Hamming distance comparison takes 0.00001 seconds, with a total 1,000,000 images, **it would take you _10 seconds_ to complete the search — _far too slow for any type of search engine._**

Instead, to build an image hashing search engine that scales, you need to utilize specialized data structures.

### What are VP-Trees and how can they help scale image hashing search engines?

[![Image 9](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20500%20557'%3E%3C/svg%3E)](https://pyimagesearch.com/wp-content/uploads/2019/08/image_hashing_search_engine_vptrees.png)

**Figure 7:** We’ll use VP-Trees for our image hash search engine using Python and OpenCV. VP-Trees are based on a recursive algorithm that computes vantage points and medians until we reach child nodes containing an individual image hash. Child nodes that are closer together (i.e. smaller Hamming Distances in our case) are assumed to be more similar to each other. ([image source](https://www.lix.polytechnique.fr/~nielsen/BregmanProximity/))

In order to scale our image hashing search engine, we need to use a specialized data structure that:

*   Reduces our search from **linear complexity**, `O(n)` …
*   …down to **sub-linear complexity**, ideally `O(log n)` .

To accomplish that task we can use **Vantage-point Trees (VP-Trees)**. VP-Trees are a metric tree that operates in a metric space by selecting a given position in space (i.e., the “vantage point”) and then partitioning the data points into two sets:

1.  Points that are _near_ the vantage point
2.  Points that are _far_ from the vantage point

We then recursively apply this process, partitioning the points into smaller and smaller sets, thus creating a tree where neighbors in the tree have _smaller distances_.

To visualize the process of constructing a VP-Tree, consider the following figure:

[![Image 10](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20500%20421'%3E%3C/svg%3E)](https://pyimagesearch.com/wp-content/uploads/2019/08/image_hashing_search_engine_vptree_steps.png)

**Figure 8:** A visual depiction of the process of building a VP-Tree (vantage point tree). We will use the [vptree](https://github.com/RickardSjogren/vptree) Python implementation by Richard Sjogren. ([image source](https://www.researchgate.net/figure/Illustration-of-the-VP-%20tree-partitioning-method-and-hierarchical-%20structure_fig2_220451935))

First, we select a point in space (denoted as the _v_ in the center of the circle) — we call this point the **vantage point**. The vantage point is the point _furthest_ from the parent vantage point in the tree.

We then compute the median, _μ_, for all points, _X_.

Once we have μ, we then divide _X_ into two sets, _S1_ and _S2_:

*   All points with distance _<= μ_ belong to _S1_.
*   All points with distance _\> μ_ belong to _S2_.

We then recursively apply this process, building a tree as we go, until we are left with a **child node**.

A child node contains only a single data point (in this case, one individual hash). Child nodes that are closer together in the tree thus have:

1.  Smaller distances between them.
2.  And therefore assumed to be _more similar_ to each other than the rest of the data points in the tree.

After recursively applying the VP-Tree construction method, we end up with a data structure, that, as the name suggests, is a tree:

[![Image 11](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20600%20229'%3E%3C/svg%3E)](https://pyimagesearch.com/wp-content/uploads/2019/08/image_hashing_search_engine_built_vptree.png)

**Figure 9:** An example VP-Tree is depicted. We will use Python to build VP-Trees for use in an image hash search engine.

Notice how we recursively split subsets of our dataset into _smaller and smaller subsets_, until we eventually reach the child nodes.

VP-Trees take `O(n log n)` to build, but once we’ve constructed it, a search takes only `O(log n)`, _thus reducing our search time to sub-linear complexity!_

Later in this tutorial, you’ll learn to utilize VP-Trees with Python to build and scale our image hashing search engine.

_**Note:** This section is meant to be a gentle introduction to VP-Trees. If you are interested in learning more about them, I would recommend (1) consulting a data structures textbook, (2) following [this guide from Steve Hanov’s blog](http://stevehanov.ca/blog/?id=130), or (3) reading [this writeup from Ivan Chen](https://fribbels.github.io/vptree/writeup)._

### The CALTECH-101 dataset

[![Image 12](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20600%20424'%3E%3C/svg%3E)](https://pyimagesearch.com/wp-content/uploads/2019/08/image_hashing_search_engine_caltech101.jpg)

**Figure 10:** The [CALTECH-101 dataset](http://www.vision.caltech.edu/Image_Datasets/Caltech101/) consists of 101 object categories. Our image hash search engine using VP-Trees, Python, and OpenCV will use the CALTECH-101 dataset for our practical example.

The dataset we’ll be working today is the [CALTECH-101 dataset](http://www.vision.caltech.edu/Image_Datasets/Caltech101/) which consists of 9,144 total images across 101 categories (with 40 to 800 images per category).

The dataset is _large enough_ to be interesting to explore from an introductory to image hashing perspective but still _small enough_ that you can run the example Python scripts in this guide without having to wait hours and hours for your system to finish chewing on the images.

You can download the CALTECH 101 dataset from their [official webpage](http://www.vision.caltech.edu/Image_Datasets/Caltech101/) or you can use the following `wget` command:

$ wget http://www.vision.caltech.edu/Image\_Datasets/Caltech101/101\_ObjectCategories.tar.gz
$ tar xvzf 101\_ObjectCategories.tar.gz

### Project structure

Let’s inspect our project structure:

$ tree --dirsfirst
.
├── pyimagesearch
│   ├── \_\_init\_\_.py
│   └── hashing.py
├── 101\_ObjectCategories \[9,144 images\] 
├── queries
│   ├── accordion.jpg
│   ├── accordion\_modified1.jpg
│   ├── accordion\_modified2.jpg
│   ├── buddha.jpg
│   └── dalmation.jpg
├── index\_images.py
└── search.py

The `pyimagesearch` module contains `hashing.py` which includes three hashing functions. We will review the functions in the _“Implementing our hashing utilities”_ section below.

Our dataset is in the `101_ObjectCategories/` folder (CALTECH-101) contains 101 sub-directories with our images. Be sure to read the previous section to learn how to download the dataset.

There are five query images in the `queries/` directory. We will search for images with similar hashes to these images. The `accordion_modified1.jpg` and `accordion_modiied2.jpg` images will present unique challenges to our VP-Trees image hashing search engine.

The core of today’s project lies in two Python scripts: `index_images.py` and `search.py` :

*   Our **indexer** will calculate hashes for all 9,144 images and organize the hashes in a VP-Tree. This index will reside in two `.pickle` files: (1) a dictionary of all computed hashes, and (2) the VP-Tree.
*   The **searcher** will calculate the hash for a query image and search the VP-Tree for the closest images via Hamming Distance. The results will be returned to the user.

If that sounds like a lot, don’t worry! This tutorial will break everything down step-by-step.

### Configuring your development environment

For this blog post, your development environment needs the following packages installed:

*   [OpenCV](https://pyimagesearch.com/2018/09/19/pip-install-opencv/)
*   [NumPy](https://www.numpy.org/)
*   [imutils](https://github.com/jrosebr1/imutils)
*   [vptree](https://github.com/RickardSjogren/vptree) (pure Python implementation of the VP-Tree data structure)

Luckily for us, everything is pip-installable. My recommendation for you is to follow the first OpenCV link to [pip-install OpenCV in a virtual environment](https://pyimagesearch.com/2018/09/19/pip-install-opencv/) on your system. From there you’ll just pip-install everything else in the same environment.

It will look something like this:

\# setup pip, virtualenv, and virtualenvwrapper (using the "pip install OpenCV" instructions)
$ workon <env\_name>
$ pip install numpy
$ pip install opencv-contrib-python
$ pip install imutils
$ pip install vptree

Replace `<env_name>` with the name of your virtual environment. The `workon` command will only be available once you set up `virtualenv` and `virtualenvwrapper` [following these instructions](https://pyimagesearch.com/2018/09/19/pip-install-opencv/).

### Implementing our image hashing utilities

Before we can build our image hashing search engine, we first need to implement a few helper utilities.

Open up the `hashing.py` file in the project structure and insert the following code:

\# import the necessary packages
import numpy as np
import cv2

def dhash(image, hashSize=8):
	# convert the image to grayscale
	gray = cv2.cvtColor(image, cv2.COLOR\_BGR2GRAY)

	# resize the grayscale image, adding a single column (width) so we
	# can compute the horizontal gradient
	resized = cv2.resize(gray, (hashSize + 1, hashSize))

	# compute the (relative) horizontal gradient between adjacent
	# column pixels
	diff = resized\[:, 1:\] > resized\[:, :-1\]

	# convert the difference image to a hash
	return sum(\[2 \*\* i for (i, v) in enumerate(diff.flatten()) if v\])

We begin by importing OpenCV and NumPy (**Lines 2 and 3**).

The first function we’ll look at, `dhash`, is used to compute the difference hash for a given input image. Recall from above that our `dhash` requires four steps: (1) convert to grayscale, (2) resize, (3) compute the difference, and (4) build the hash. Let’s break it down a little further:

1.  **Line 7** converts the image to _**grayscale**_.
2.  **Line 11** _**resizes**_ the image to _N + 1_ rows by _N_ columns, ignoring the aspect ratio. This ensures that the resulting image hash will match similar photos _regardless_ of their initial spatial dimensions.
3.  **Line 15** computes the _**horizontal gradient difference**_ between adjacent column pixels. Assuming `hashSize=8` , will be 8 rows of 8 differences (there are 9 rows allowing for 8 comparisons). We will thus have a 64-bit hash as 8×8=64.
4.  **Line 18** converts the difference image to a _**hash**_.

For more details, refer to [this blog post](https://pyimagesearch.com/2017/11/27/image-hashing-opencv-python/).

Next, let’s look at `convert_hash` function:

def convert\_hash(h):
	# convert the hash to NumPy's 64-bit float and then back to
	# Python's built in int
	return int(np.array(h, dtype="float64"))

When I first wrote the code for this tutorial, I found that the VP-Tree implementation we’re using internally converts points to a NumPy 64-bit float. That would be okay; however, hashes need to be integers and if we convert them to 64-bit floats, they become an unhashable data type. To overcome the limitation of the VP-Tree implementation, I came up with the `convert_hash` hack:

*   We accept an input hash, `h`.
*   That hash is then converted to a NumPy 64-bit float.
*   And that NumPy float is then converted back to Python’s built-in integer data type.

This hack ensures that hashes are represented consistently throughout the hashing, indexing, and searching process.

We then have one final helper method, `hamming`, which is used to compute the Hamming distance between two integers:

def hamming(a, b):
	# compute and return the Hamming distance between the integers
	return bin(int(a) ^ int(b)).count("1")

The Hamming distance is simply a `count` of the number of 1s when taking the XOR (`^`) between two integers (**Line 27**).

### Implementing our image hash indexer

Before we can perform a search, we first need to:

1.  Loop over our input dataset of images.
2.  Compute difference hash for each image.
3.  Build a VP-Tree using the hashes.

Let’s start that process now.

Open up the `index_images.py` file and insert the following code:

\# import the necessary packages
from pyimagesearch.hashing import convert\_hash
from pyimagesearch.hashing import hamming
from pyimagesearch.hashing import dhash
from imutils import paths
import argparse
import pickle
import vptree
import cv2

# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add\_argument("-i", "--images", required=True, type=str,
	help="path to input directory of images")
ap.add\_argument("-t", "--tree", required=True, type=str,
	help="path to output VP-Tree")
ap.add\_argument("-a", "--hashes", required=True, type=str,
	help="path to output hashes dictionary")
args = vars(ap.parse\_args())

**Lines 2-9** import the packages, functions, and modules necessary for this script. In particular **Lines 2-4** import our three hashing related functions: `convert_hash` , `hamming` , and `dhash` . **Line 8** imports the `vptree` implementation that we will be using.

Next, **Lines 12-19** parse our command line arguments:

*   `--images` : The path to our images which we will be indexing.
*   `--tree` : The path to the output VP-tree `.pickle` file which will be serialized to disk.
*   `--hashes` : The path to the output hashes dictionary which will be stored in `.pickle` format.

Now let’s compute hashes for all images:

\# grab the paths to the input images and initialize the dictionary
# of hashes
imagePaths = list(paths.list\_images(args\["images"\]))
hashes = {}

# loop over the image paths
for (i, imagePath) in enumerate(imagePaths):
	# load the input image
	print("\[INFO\] processing image {}/{}".format(i + 1,
		len(imagePaths)))
	image = cv2.imread(imagePath)

	# compute the hash for the image and convert it
	h = dhash(image)
	h = convert\_hash(h)

	# update the hashes dictionary
	l = hashes.get(h, \[\])
	l.append(imagePath)
	hashes\[h\] = l

**Lines 23 and 24** grab image paths and initialize our `hashes` dictionary.

**Line 27** then begins a loop over all the `imagePaths` . Inside the loop, we:

*   Load the `image` (**Line 31**).
*   Compute and convert the hash, `h` (**Lines 34 and 35**).
*   Grab a list of all image paths, `l` , with the same hash (**Line 38**).
*   Add this `imagePath` to the list, `l` (**Line 39**).
*   Update our dictionary with the hash as the key and our list of image paths with the same corresponding hash as the value (**Line 40**).

From here, we build our VP-Tree:

\# build the VP-Tree
print("\[INFO\] building VP-Tree...")
points = list(hashes.keys())
tree = vptree.VPTree(points, hamming)

To construct the VP-Tree, **Lines 44 and 45** pass in (1) a list of data points (i.e., the hash integer values themselves), and (2) our distance function (the Hamming distance method) to the `VPTree` constructor.

Internally, the VP-Tree computes the Hamming distances between all input `points` and then constructs the VP-Tree such that data points with smaller distances (i.e., more similar images) lie closer together in the tree space. Be sure to refer the _“What are VP-Trees and how can they help scale image hashing search engines?”_ section and **Figures 7, 8, and 9**.

With our `hashes` dictionary populated and VP-Tree constructed, we’ll now serialize them both to disk as `.pickle` files:

\# serialize the VP-Tree to disk
print("\[INFO\] serializing VP-Tree...")
f = open(args\["tree"\], "wb")
f.write(pickle.dumps(tree))
f.close()

# serialize the hashes to dictionary
print("\[INFO\] serializing hashes...")
f = open(args\["hashes"\], "wb")
f.write(pickle.dumps(hashes))
f.close()

### Extracting image hashes and building the VP-Tree

Now that we’ve implemented our indexing script, let’s put it to work. Make sure you’ve:

1.  Downloaded the CALTECH-101 dataset using the instructions above.
2.  Used the **_“Downloads”_** section of this tutorial to download the source code and example query images.
3.  Extracted the .zip of the source code and changed directory to the project.

From there, open up a terminal and issue the following command:

$ time python index\_images.py --images 101\_ObjectCategories \\
	--tree vptree.pickle --hashes hashes.pickle
\[INFO\] processing image 1/9144
\[INFO\] processing image 2/9144
\[INFO\] processing image 3/9144
\[INFO\] processing image 4/9144
\[INFO\] processing image 5/9144
...
\[INFO\] processing image 9140/9144
\[INFO\] processing image 9141/9144
\[INFO\] processing image 9142/9144
\[INFO\] processing image 9143/9144
\[INFO\] processing image 9144/9144
\[INFO\] building VP-Tree...
\[INFO\] serializing VP-Tree...
\[INFO\] serializing hashes...

real	0m10.947s
user	0m9.096s
sys		0m1.386s

As our output indicates, we were able to hash all 9,144 images in just over 10 seconds.

Checking project directory after running the script we’ll find two `.pickle` files:

$ ls -l \*.pickle
-rw-r--r--  1 adrianrosebrock  796620 Aug 22 07:53 hashes.pickle
-rw-r--r--  1 adrianrosebrock  707926 Aug 22 07:53 vptree.pickle

The `hashes.pickle` (796.62KB) file contains our computed hashes, mapping the hash integer value to file paths with the same hash. The `vptree.pickle` (707.926KB) is our constructed VP-Tree.

We’ll be using this VP-Tree to perform queries and searches in the following section.

### Implementing our image hash searching script

The second component of an image hashing search engine is the search script. The search script will:

1.  Accept an input query image.
2.  Compute the hash for the query image.
3.  Search the VP-Tree using the query hash to find all duplicate/near-duplicate images.

Let’s implement our image hash searcher now — open up the `search.py` file and insert the following code:

\# import the necessary packages
from pyimagesearch.hashing import convert\_hash
from pyimagesearch.hashing import dhash
import argparse
import pickle
import time
import cv2

# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add\_argument("-t", "--tree", required=True, type=str,
	help="path to pre-constructed VP-Tree")
ap.add\_argument("-a", "--hashes", required=True, type=str,
	help="path to hashes dictionary")
ap.add\_argument("-q", "--query", required=True, type=str,
	help="path to input query image")
ap.add\_argument("-d", "--distance", type=int, default=10,
	help="maximum hamming distance")
args = vars(ap.parse\_args())

**Lines 2-9** import the necessary components for our searching script. Notice that we need the `dhash` and `convert_hash` functions once again as we’ll have to compute the hash for our `--query` image.

**Lines 10-19** parse our command line arguments (the first three are required):

*   `--tree` : The path to our pre-constructed VP-Tree on disk.
*   `--hashes` : The path to our pre-computed hashes dictionary on disk.
*   `--query` : Our query image’s path.
*   `--distance` : The maximum hamming distance between hashes is set with a `default` of `10` . You may override it if you so choose.

It’s important to note that the _larger_ the `--distance` is, the _more_ hashes the VP-Tree will compare, and thus the searcher will be _slower._ Try to keep your `--distance` as _small as possible_ without compromising the quality of your results.

Next, we’ll (1) load our VP-Tree + hashes dictionary, and (2) compute the hash for our `--query` image:

\# load the VP-Tree and hashes dictionary
print("\[INFO\] loading VP-Tree and hashes...")
tree = pickle.loads(open(args\["tree"\], "rb").read())
hashes = pickle.loads(open(args\["hashes"\], "rb").read())

# load the input query image
image = cv2.imread(args\["query"\])
cv2.imshow("Query", image)

# compute the hash for the query image, then convert it
queryHash = dhash(image)
queryHash = convert\_hash(queryHash)

**Lines 23 and 24** load the pre-computed index including the VP-Tree and `hashes` dictionary.

From there, we load and display the `--query` image (**Lines 27 and 28**).

We then take the query `image` and compute the `queryHash` (**Lines 31 and 32**).

At this point, it is time to **perform a search using our VP-Tree:**

\# perform the search
print("\[INFO\] performing search...")
start = time.time()
results = tree.get\_all\_in\_range(queryHash, args\["distance"\])
results = sorted(results)
end = time.time()
print("\[INFO\] search took {} seconds".format(end - start))

**Lines 37 and 38** perform a search by querying the VP-Tree for hashes with the smallest Hamming distance relative to the `queryHash` . The `results` are `sorted` so that “more similar” hashes are at the front of the `results` list.

Both of these lines are sandwiched with timestamps for benchmarking purposes, the results of which are printed via **Line 40**.

Finally, we will loop over `results` and display each of them:

\# loop over the results
for (d, h) in results:
	# grab all image paths in our dataset with the same hash
	resultPaths = hashes.get(h, \[\])
	print("\[INFO\] {} total image(s) with d: {}, h: {}".format(
		len(resultPaths), d, h))

	# loop over the result paths
	for resultPath in resultPaths:
		# load the result image and display it to our screen
		result = cv2.imread(resultPath)
		cv2.imshow("Result", result)
		cv2.waitKey(0)

**Line 43** begins a loop over the `results` :

*   The `resultPaths` for the current hash, `h` , are grabbed from the hashes dictionary (**Line 45**).
*   Each `result` image is displayed as a key is pressed on the keyboard (**Lines 50-54**).

### Image hashing search engine results

We are now ready to test our image search engine!

But before we do that, make sure you have:

1.  Downloaded the CALTECH-101 dataset using the instructions above.
2.  Used the **_“Downloads”_** section of this tutorial to download the source code and example query images.
3.  Extracted the .zip of the source code and changed directory to the project.
4.  Ran the `index_images.py` file to generate the `hashes.pickle` and `vptree.pickle` files.

After all the above steps are complete, open up a terminal and execute the following command:

python search.py --tree vptree.pickle --hashes hashes.pickle \\
	--query queries/buddha.jpg
\[INFO\] loading VP-Tree and hashes...
\[INFO\] performing search...
\[INFO\] search took 0.015203237533569336 seconds
\[INFO\] 1 total image(s) with d: 0, h: 8.162938100012111e+18

[![Image 13](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20600%20383'%3E%3C/svg%3E)](https://pyimagesearch.com/wp-content/uploads/2019/08/image_hashing_search_engine_result01.jpg)

**Figure 11:** Our Python + OpenCV image hashing search engine found a match in the VP-Tree in just 0.015 seconds!

On the _left,_ you can see our input query image of our Buddha. On the _right,_ you can see that we have found the duplicate image in our indexed dataset.

**The search itself took only 0.015 seconds.**

Additionally, note that the distance between the input query image and the hashed image in the dataset is zero, **indicating that the two images are _identical_.**

Let’s try again, this time with an image of a Dalmatian:

$ python search.py --tree vptree.pickle --hashes hashes.pickle \\
	--query queries/dalmation.jpg 
\[INFO\] loading VP-Tree and hashes...
\[INFO\] performing search...
\[INFO\] search took 0.014827728271484375 seconds
\[INFO\] 1 total image(s) with d: 0, h: 6.445556196029652e+18

[![Image 14](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20600%20390'%3E%3C/svg%3E)](https://pyimagesearch.com/wp-content/uploads/2019/08/image_hashing_search_engine_result02.jpg)

**Figure 12:** With a Hamming Distance of 0, the Dalmation query image yielded an identical image in our dataset. We built an OpenCV + Python image hash search engine with VP-Trees successfully.

Again, we see that our image hashing search engine has found the identical Dalmatian in our indexed dataset (we know the images are identical due to the Hamming distance of zero).

The next example is of an accordion:

$ python search.py --tree vptree.pickle --hashes hashes.pickle \\
	--query queries/accordion.jpg 
\[INFO\] loading VP-Tree and hashes...
\[INFO\] performing search...
\[INFO\] search took 0.014187097549438477 seconds
\[INFO\] 1 total image(s) with d: 0, h: 3.380309217342405e+18

[![Image 15](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20600%20386'%3E%3C/svg%3E)](https://pyimagesearch.com/wp-content/uploads/2019/08/image_hashing_search_engine_result03.jpg)

**Figure 13:** An example of providing a query image and finding the best resulting image with an image hash search engine created with Python and OpenCV.

We once again find our identical matched image in the indexed dataset.

**We know our image hashing search engine is working great for identical images…**

_**…but what about images that are slightly modified?**_

Will our hashing search engine still perform well?

Let’s give it a try:

$ python search.py --tree vptree.pickle --hashes hashes.pickle \\
	--query queries/accordion\_modified1.jpg 
\[INFO\] loading VP-Tree and hashes...
\[INFO\] performing search...
\[INFO\] search took 0.014217138290405273 seconds
\[INFO\] 1 total image(s) with d: 4, h: 3.380309217342405e+18

[![Image 16](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20600%20393'%3E%3C/svg%3E)](https://pyimagesearch.com/wp-content/uploads/2019/08/image_hashing_search_engine_result04-1.jpg)

**Figure 14:** Our image hash search engine was able to find the matching image despite a modification (red square) to the query image.

Here I’ve added a small red square in the bottom left corner of the accordion query image. This addition will **_change the difference hash value!_**

However, if you take a look at the output result, you’ll see that we were _still_ able to detect the near-duplicate image.

We were able to find the near-duplicate image by comparing the Hamming distance between the hashes. The difference in hash values is 4, indicating that 4 bits differ between the two hashes.

Next, let’s try a second query, **this one _much_ more modified than the first:**

$ python search.py --tree vptree.pickle --hashes hashes.pickle \\
	--query queries/accordion\_modified2.jpg 
\[INFO\] loading VP-Tree and hashes...
\[INFO\] performing search...
\[INFO\] search took 0.013727903366088867 seconds
\[INFO\] 1 total image(s) with d: 9, h: 3.380309217342405e+18

[![Image 17](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20600%20394'%3E%3C/svg%3E)](https://pyimagesearch.com/wp-content/uploads/2019/08/image_hashing_search_engine_result05.jpg)

**Figure 15:** On the _left_ is the query image for our image hash search engine with VP-Trees. It has been modified with yellow and purple shapes as well as red text. The image hash search engine returns the correct resulting image (_right_) from an index of 9,144 in just 0.0137 seconds, proving the robustness of our search engine system.

Despite dramatically altering the query by adding in a large blue rectangle, a yellow circle, and text, we’re still able to find the near-duplicate image in our dataset in under 0.014 seconds!

Whenever you need to find duplicate or near-duplicate images in a dataset, definitely consider using image hashing and image searching algorithms — when used correctly, they can be extremely powerful!

### What's next? We recommend [PyImageSearch University](https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend).

**Course information:**  
84 total classes • 114+ hours of on-demand code walkthrough videos • Last updated: February 2024  
★★★★★ 4.84 (128 Ratings) • 16,000+ Students Enrolled

**I strongly believe that if you had the right teacher you could _master_ computer vision and deep learning.**

Do you think learning computer vision and deep learning has to be time-consuming, overwhelming, and complicated? Or has to involve complex mathematics and equations? Or requires a degree in computer science?

That’s _not_ the case.

All you need to master computer vision and deep learning is for someone to explain things to you in _simple, intuitive_ terms. _And that’s exactly what I do_. My mission is to change education and how complex Artificial Intelligence topics are taught.

If you're serious about learning computer vision, your next stop should be PyImageSearch University, the most comprehensive computer vision, deep learning, and OpenCV course online today. Here you’ll learn how to _successfully_ and _confidently_ apply computer vision to your work, research, and projects. Join me in computer vision mastery.

**Inside PyImageSearch University you'll find:**

*   ✓ **84 courses** on essential computer vision, deep learning, and OpenCV topics
*   ✓ **84 Certificates** of Completion
*   ✓ **114+ hours** of on-demand video
*   ✓ **Brand new courses released _regularly_**, ensuring you can keep up with state-of-the-art techniques
*   ✓ **Pre-configured Jupyter Notebooks in Google Colab**
*   ✓ Run all code examples in your web browser — works on Windows, macOS, and Linux (no dev environment configuration required!)
*   ✓ Access to **centralized code repos for _all_ 536+ tutorials** on PyImageSearch
*   ✓ **Easy one-click downloads** for code, datasets, pre-trained models, etc.
*   ✓ **Access** on mobile, laptop, desktop, etc.

[Click here to join PyImageSearch University](https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend)

Summary
-------

In this tutorial, you learned how to build a basic image hashing search engine using OpenCV and Python.

To build an image hashing search engine that scaled we needed to utilize VP- Trees, a specialized metric tree data structure that recursively partitions a dataset of points such that nodes of the tree that are _closer together_ are _more similar_ than nodes that are farther away.

By using VP-Trees we were able to build an image hashing search engine capable of finding duplicate and near-duplicate images in a dataset in under 0.01 seconds.

Furthermore, we demonstrated that our combination of hashing algorithm and VP-Tree search was capable of finding matches in our dataset, _**even if our query image was modified, damaged, or altered!**_

If you are ever building a computer vision application that requires quickly finding duplicate or near-duplicate images in a large dataset, definitely give this method a try.

**To download the source code to this post, and be notified when future posts are published here on PyImageSearch, _just enter your email address in the form below!_**

![Image 18](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20410%20567'%3E%3C/svg%3E)

#### Download the Source Code and FREE 17-page Resource Guide

Enter your email address below to get a .zip of the code and a **FREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning.** Inside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL!