christiangeorgelucas/image-hash-tools
v0.1.0PythonThe image-hash-tools package provides a suite of composable perceptual image-hashing nodes designed for efficient near-duplicate detection. By utilizing various hashing techniques, such as average, difference, and perceptual hashing, it enables robust content-similarity comparisons that are resistant to common image transformations.
Use cases
- •Detect near-duplicate images in a large dataset
- •Implement content-based image retrieval systems
- •Perform image similarity checks for copyright enforcement
- •Create a system for organizing images by visual similarity
- •Enable efficient image deduplication in storage solutions
Nodes (8)
The AverageHash node computes the average hash (aHash) of an image by resizing it to a small grayscale grid and thresholding each pixel against the mean value. This method is fast, robust to scaling and gamma changes, and produces a deterministic output.
View source
from gen.messages_pb2 import AverageHashInput, HashResult
from gen.axiom_context import AxiomContext
from nodes._imaging import load_pil
from nodes._hashing import compute_square_hash, hash_result_fields
def average_hash(ax: AxiomContext, input: AverageHashInput) -> HashResult:
"""Average hash (aHash): resize to hash_size x hash_size grayscale, threshold
each pixel against the mean. Fast and robust to scaling/gamma changes, but
the least discriminative of the luminance hashes. hash_size defaults to 8
(a 64-bit hash) when 0. Deterministic: the same image + hash_size always
produce the same hex-encoded hash.
"""
pil = load_pil(input.image)
h, size = compute_square_hash("ahash", pil, input.hash_size)
return HashResult(**hash_result_fields(h, "ahash", size))
The ColorHash node computes a position-independent color hash from an image's HSV histogram, focusing on the black/gray fraction and saturated-hue bins. This allows for matching images by color palette regardless of their layout, ensuring deterministic results.
View source
from gen.messages_pb2 import ColorHashInput, HashResult
from gen.axiom_context import AxiomContext
from nodes._imaging import load_pil
from nodes._hashing import DEFAULT_BINBITS, hash_result_fields, resolve_size
import imagehash
def color_hash(ax: AxiomContext, input: ColorHashInput) -> HashResult:
"""Color hash: bins the black/gray fraction plus hue histograms of highly-
and mildly-saturated pixels from the image's HSV histogram. Unlike the
luminance hashes (aHash/dHash/pHash/wHash), this is position-independent —
it hashes the color *distribution*, not a spatial grid, so it is useful
for matching by color palette regardless of layout. binbits (bits of
resolution per bin) defaults to 3 when 0. Deterministic: the same image +
binbits always produce the same hex-encoded hash.
"""
pil = load_pil(input.image)
binbits = resolve_size(input.binbits, DEFAULT_BINBITS)
h = imagehash.colorhash(pil, binbits=binbits)
return HashResult(**hash_result_fields(h, "colorhash", binbits))
The CropResistantHash node segments an image into bright and dark regions, applying a difference-hashing technique to each segment. This approach ensures that the resulting hash remains matchable even after significant cropping, offering greater tolerance compared to traditional single-hash algorithms.
View source
from gen.messages_pb2 import CropResistantHashInput, HashResult
from gen.axiom_context import AxiomContext
from nodes._imaging import load_pil
from nodes._hashing import (
DEFAULT_MIN_SEGMENT_SIZE,
DEFAULT_SEGMENTATION_IMAGE_SIZE,
crop_resistant_hash_func,
hash_result_fields,
resolve_size,
)
import imagehash
def crop_resistant_hash(ax: AxiomContext, input: CropResistantHashInput) -> HashResult:
"""Crop-resistant hash: segments the image into bright/dark regions (a
watershed-like split) and difference-hashes each segment, so the overall
result stays matchable even after moderate cropping (the paper reports
tolerance up to ~50% crop, vs ~5% for the single-hash algorithms above).
The returned `hash` is a comma-joined list of per-segment hex hashes —
reconstruct it with `imagehash.hex_to_multihash` if consuming outside this
package. min_segment_size defaults to 500px, segmentation_image_size to
300px, and the per-segment hash_size to 8, all when passed as 0.
Deterministic: the same image + params always produce the same segments
and hashes.
"""
pil = load_pil(input.image)
min_segment_size = resolve_size(input.min_segment_size, DEFAULT_MIN_SEGMENT_SIZE)
segmentation_image_size = resolve_size(input.segmentation_image_size, DEFAULT_SEGMENTATION_IMAGE_SIZE)
hash_func, size = crop_resistant_hash_func(input.hash_size)
h = imagehash.crop_resistant_hash(
pil,
hash_func=hash_func,
min_segment_size=min_segment_size,
segmentation_image_size=segmentation_image_size,
)
return HashResult(**hash_result_fields(h, "crop_resistant", size))
The DifferenceHash node computes a difference hash (dHash) of an image by resizing it to a small grayscale grid and comparing each pixel to its left neighbor, providing a cheap and effective method for identifying near-duplicate images.
View source
from gen.messages_pb2 import DifferenceHashInput, HashResult
from gen.axiom_context import AxiomContext
from nodes._imaging import load_pil
from nodes._hashing import compute_square_hash, hash_result_fields
def difference_hash(ax: AxiomContext, input: DifferenceHashInput) -> HashResult:
"""Difference hash (dHash): resize to (hash_size+1) x hash_size grayscale,
threshold each pixel against its left neighbor. Cheap and a good general-
purpose near-duplicate hash. hash_size defaults to 8 (a 64-bit hash) when
0. Deterministic: the same image + hash_size always produce the same
hex-encoded hash.
"""
pil = load_pil(input.image)
h, size = compute_square_hash("dhash", pil, input.hash_size)
return HashResult(**hash_result_fields(h, "dhash", size))
The HashDistance node computes the Hamming distance and similarity between two hex-encoded hashes generated by specific hashing algorithms, allowing for image comparison without the need for re-hashing.
View source
from gen.messages_pb2 import HashDistanceInput, HashDistanceResult
from gen.axiom_context import AxiomContext
from nodes._hashing import SQUARE_ALGORITHMS, reconstruct_colorhash, reconstruct_square_hash
def hash_distance(ax: AxiomContext, input: HashDistanceInput) -> HashDistanceResult:
"""Hamming distance between two hex-encoded hashes produced by this
package's AverageHash/DifferenceHash/PerceptualHash/WaveletHash/ColorHash
nodes. Lower distance means more similar; distance 0 means identical
hashes. `algorithm` must match how BOTH hashes were computed ("ahash" |
"dhash" | "phash" | "whash" | "colorhash"); for "colorhash", `hash_size`
must equal the original `binbits`. crop_resistant hashes are not
supported here — they need segment-aware comparison, not a flat hamming
distance. Deterministic: the same two hashes always produce the same
distance.
"""
algorithm = input.algorithm
if algorithm in SQUARE_ALGORITHMS:
hash_a = reconstruct_square_hash(input.hash_a)
hash_b = reconstruct_square_hash(input.hash_b)
elif algorithm == "colorhash":
hash_a = reconstruct_colorhash(input.hash_a, input.hash_size)
hash_b = reconstruct_colorhash(input.hash_b, input.hash_size)
else:
raise ValueError(
f"unknown algorithm {algorithm!r}; expected one of "
f"{SQUARE_ALGORITHMS + ('colorhash',)}"
)
try:
distance = hash_a - hash_b
except TypeError as e:
raise ValueError(f"hash_a and hash_b are not comparable: {e}") from e
max_bits = len(hash_a)
similarity = 1.0 - (distance / max_bits) if max_bits else 0.0
return HashDistanceResult(distance=distance, max_bits=max_bits, similarity=similarity)
The NearDuplicate node hashes two images using a specified algorithm and determines if they are near-duplicates based on a defined Hamming distance threshold, providing a single call solution for this comparison.
View source
from gen.messages_pb2 import NearDuplicateInput, NearDuplicateResult
from gen.axiom_context import AxiomContext
from nodes._imaging import load_pil
from nodes._hashing import DEFAULT_ALGORITHM, DEFAULT_THRESHOLD, SQUARE_ALGORITHMS, compute_square_hash, resolve_size
def near_duplicate(ax: AxiomContext, input: NearDuplicateInput) -> NearDuplicateResult:
"""Convenience node: hashes both images with the same luminance algorithm
("ahash" | "dhash" | "phash" | "whash", default "phash" when empty) and
hash_size (default 8), then reports whether their hamming distance is
within `threshold` (default 10 — a commonly-used cutoff for a 64-bit
luminance hash; lower is stricter). Equivalent to calling the matching
hash node twice plus HashDistance, in one call. Deterministic: the same
two images + params always produce the same verdict.
"""
algorithm = input.algorithm or DEFAULT_ALGORITHM
if algorithm not in SQUARE_ALGORITHMS:
raise ValueError(f"unknown algorithm {algorithm!r}; expected one of {SQUARE_ALGORITHMS}")
threshold = resolve_size(input.threshold, DEFAULT_THRESHOLD)
pil_a = load_pil(input.image_a)
pil_b = load_pil(input.image_b)
hash_a, _ = compute_square_hash(algorithm, pil_a, input.hash_size)
hash_b, _ = compute_square_hash(algorithm, pil_b, input.hash_size)
distance = hash_a - hash_b
return NearDuplicateResult(
is_near_duplicate=distance <= threshold,
distance=distance,
threshold=threshold,
algorithm=algorithm,
hash_a=str(hash_a),
hash_b=str(hash_b),
)
The PerceptualHash node computes a perceptual hash (pHash) of an image by applying a 2D Discrete Cosine Transform (DCT) to a downscaled grayscale version of the image, and thresholds the low-frequency coefficients against their median. This method is widely used for identifying near-duplicate images, as it is robust to variations in color, contrast, and compression.
View source
from gen.messages_pb2 import PerceptualHashInput, HashResult
from gen.axiom_context import AxiomContext
from nodes._imaging import load_pil
from nodes._hashing import compute_square_hash, hash_result_fields
def perceptual_hash(ax: AxiomContext, input: PerceptualHashInput) -> HashResult:
"""Perceptual hash (pHash): 2D DCT of a downscaled grayscale image,
threshold the low-frequency coefficients against their median. The most
widely used general-purpose near-duplicate hash — more robust to color/
contrast/compression changes than aHash or dHash. hash_size defaults to 8
(a 64-bit hash) when 0. Deterministic: the same image + hash_size always
produce the same hex-encoded hash.
"""
pil = load_pil(input.image)
h, size = compute_square_hash("phash", pil, input.hash_size)
return HashResult(**hash_result_fields(h, "phash", size))
The WaveletHash node computes the wavelet hash (wHash) of an image using a discrete wavelet transform, specifically Haar or Daubechies-4, on a downscaled grayscale version of the image. This method is designed to be robust against minor alterations such as crops or rotations, producing a deterministic hash output.
View source
from gen.messages_pb2 import WaveletHashInput, HashResult
from gen.axiom_context import AxiomContext
from nodes._imaging import load_pil
from nodes._hashing import compute_square_hash, hash_result_fields
def wavelet_hash(ax: AxiomContext, input: WaveletHashInput) -> HashResult:
"""Wavelet hash (wHash): discrete wavelet transform (Haar or Daubechies-4)
of a downscaled grayscale image, threshold the low-frequency coefficients
against their median. Often more robust to small crops/rotations than
pHash. hash_size must be a power of 2, defaults to 8 (a 64-bit hash) when
0. `mode` is "haar" (default) or "db4". Deterministic: the same image +
hash_size + mode always produce the same hex-encoded hash.
"""
pil = load_pil(input.image)
h, size = compute_square_hash("whash", pil, input.hash_size, mode=input.mode)
return HashResult(**hash_result_fields(h, "whash", size))
Messages (12)
Download .protoThe input image for which the average hash will be computed.
The size of the grid used for computing the hash; defaults to 8 for a 64-bit hash.
The input image for which the color hash will be computed.
The number of bits of resolution per bin, which determines the granularity of the hash.
The input image to be processed for generating the crop-resistant hash.
The minimum size of each segment in pixels; defaults to 500px if not specified.
The size of the image used for segmentation; defaults to 300px if not specified.
The size of the hash for each segment, which determines the granularity of the hashing.
The input image for which the difference hash will be computed.
The size of the hash grid; defaults to 8, resulting in a 64-bit hash if set to 0.
The first hex-encoded hash to compare.
The second hex-encoded hash to compare.
Specifies the hashing algorithm used to generate the input hashes, which must match for both hashes.
The size of the hash used when the colorhash algorithm is applied.
The size of the grid used for computing the hash; defaults to 8 for a 64-bit hash.
The first image to be compared for near-duplication.
The second image to be compared for near-duplication.
The hashing algorithm used for generating the image hashes, with options including 'ahash', 'dhash', 'phash', and 'whash'.
The size of the hash to be computed, which affects the granularity of the comparison.
The maximum allowed Hamming distance for the images to be considered near-duplicates.
The maximum allowed Hamming distance for the images to be considered near-duplicates.
The hashing algorithm used for generating the image hashes, with options including 'ahash', 'dhash', 'phash', and 'whash'.
The input image for which the perceptual hash is to be computed.
The size of the hash to be generated; defaults to 8 for a 64-bit hash if set to 0.
The input image for which the wavelet hash will be computed.
The size of the hash, which must be a power of 2; defaults to 8 for a 64-bit hash if set to 0.
The wavelet transform mode to use, either 'haar' (default) or 'db4'.
Use as a tool in any AI agent
christiangeorgelucas/image-hash-tools is callable as a tool from any MCP client (Claude, Cursor, or your own agent) via Axiom's hosted MCP server — no install, no download. Add the server, then your agent can search and invoke it, or pin it as a typed tool.
claude mcp add --transport http axiom https://api.axiomide.com/mcp --header "Authorization: Bearer YOUR_AXIOM_API_KEY"Replace YOUR_AXIOM_API_KEY with a key you create in Console → API Keys.
MCP server setup & other clients →Use this package
Sign in to import christiangeorgelucas/image-hash-tools into the graph editor — drop it into a flow and hit run. Or call it directly via the API Reference with your API key.
Sign in to useOpen editor