Public beta — not for production use. Data may be wiped at any time. Questions? Contact us.
A
Axiom
/Marketplace/christiangeorgelucas/image-hash-tools
Sign in

christiangeorgelucas/image-hash-tools

v0.1.0Python

The 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.

image-hashingperceptual-hashingnear-duplicate-detectioncontent-similaritydeterministic-hashingimage-processinghashing-algorithms

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)

AverageHashunary
AverageHashInput·HashResult

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))
ColorHashunary
ColorHashInput·HashResult

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))
CropResistantHashunary
CropResistantHashInput·HashResult

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))
DifferenceHashunary
DifferenceHashInput·HashResult

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))
HashDistanceunary
HashDistanceInput·HashDistanceResult

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)
NearDuplicateunary
NearDuplicateInput·NearDuplicateResult

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),
    )
PerceptualHashunary
PerceptualHashInput·HashResult

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))
WaveletHashunary
WaveletHashInput·HashResult

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 .proto
AverageHashInput
image:Image

The input image for which the average hash will be computed.

hash_size:int32

The size of the grid used for computing the hash; defaults to 8 for a 64-bit hash.

ColorHashInput
image:Image

The input image for which the color hash will be computed.

binbits:int32

The number of bits of resolution per bin, which determines the granularity of the hash.

CropResistantHashInput
image:Image

The input image to be processed for generating the crop-resistant hash.

min_segment_size:int32

The minimum size of each segment in pixels; defaults to 500px if not specified.

segmentation_image_size:int32

The size of the image used for segmentation; defaults to 300px if not specified.

hash_size:int32

The size of the hash for each segment, which determines the granularity of the hashing.

DifferenceHashInput
image:Image

The input image for which the difference hash will be computed.

hash_size:int32

The size of the hash grid; defaults to 8, resulting in a 64-bit hash if set to 0.

HashDistanceInput
hash_a:string

The first hex-encoded hash to compare.

hash_b:string

The second hex-encoded hash to compare.

algorithm:string

Specifies the hashing algorithm used to generate the input hashes, which must match for both hashes.

hash_size:int32

The size of the hash used when the colorhash algorithm is applied.

HashDistanceResult
distance:int32
max_bits:int32
similarity:double
HashResult
hash:string
algorithm:string
hash_size:int32

The size of the grid used for computing the hash; defaults to 8 for a 64-bit hash.

Image
data:bytes
format:string
width:int32
height:int32
url:string
NearDuplicateInput
image_a:Image

The first image to be compared for near-duplication.

image_b:Image

The second image to be compared for near-duplication.

algorithm:string

The hashing algorithm used for generating the image hashes, with options including 'ahash', 'dhash', 'phash', and 'whash'.

hash_size:int32

The size of the hash to be computed, which affects the granularity of the comparison.

threshold:int32

The maximum allowed Hamming distance for the images to be considered near-duplicates.

NearDuplicateResult
is_near_duplicate:bool
distance:int32
threshold:int32

The maximum allowed Hamming distance for the images to be considered near-duplicates.

algorithm:string

The hashing algorithm used for generating the image hashes, with options including 'ahash', 'dhash', 'phash', and 'whash'.

hash_a:string
hash_b:string
PerceptualHashInput
image:Image

The input image for which the perceptual hash is to be computed.

hash_size:int32

The size of the hash to be generated; defaults to 8 for a 64-bit hash if set to 0.

WaveletHashInput
image:Image

The input image for which the wavelet hash will be computed.

hash_size:int32

The size of the hash, which must be a power of 2; defaults to 8 for a 64-bit hash if set to 0.

mode:string

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

Stats

Error rate (1h)0.0%
Back to marketplace