Skip to content

Comparison of wHash with the Python implementation #32

Description

@ava57r

Hello.
I try to compare hashes, but i have a difference.
I asked Ai.

Results =>

Comparing whash: Python imagehash vs Rust imgdd

Short answer: no, these are not the same implementation. Hashes may match only in narrow edge cases; under normal use they will differ.


Python imagehash.whash

Source: imagehash/init.py

def whash(image, hash_size=8, image_scale=None, mode='haar', remove_max_haar_ll=True):
    image = image.convert('L').resize((image_scale, image_scale), ANTIALIAS)
    pixels = numpy.asarray(image) / 255.

    if remove_max_haar_ll:
        coeffs = pywt.wavedec2(pixels, 'haar', level=ll_max_level)
        coeffs[0] *= 0
        pixels = pywt.waverec2(coeffs, 'haar')

    coeffs = pywt.wavedec2(pixels, mode, level=dwt_level)
    dwt_low = coeffs[0]

    med = numpy.median(dwt_low)
    diff = dwt_low > med
    return ImageHash(diff)

Rust imgdd::ImageHash::whash

Source: hashing.rs#L204

Before calling it, dedupe.rs always resizes the image to 8×8:

let normalized = normalize::proc(&image, filter, 8, 8)?;
ImageHash::whash(&normalized)
pub fn whash(image: &DynamicImage) -> Result<Self> {
    // 8×8, normalized to 0..1
    pixels.transform(Operation::Forward, &Haar::new(), 3);
    pixels[0] = 0.0;
    pixels.transform(Operation::Inverse, &Haar::new(), 3);

    // median over all 64 pixels after inverse
    // hash: val > median
}

Key differences

Aspect Python imagehash Rust imgdd
Size before DWT image_scale — largest power of 2 up to image size (512px → 512×512) Always 8×8
What gets hashed LL subband after the second wavedec2: coeffs[0] (8×8 coefficients) Spatial-domain pixels after inverse DWT
DWT stages Two passes: (1) remove max LL + reconstruct, (2) another DWT and take LL One pass: forward → zero DC → inverse
Library PyWavelets (pywt) dwt + Haar
Resize LANCZOS (ANTIALIAS) Configurable filter (Lanczos3, etc.)
Parameters hash_size, image_scale, mode, remove_max_haar_ll Fixed: 8×8, Haar, level=3

Why hashes do not match

1. Different scale (main reason)

For a 1024×1024 image, Python by default does:

image_scale = 1024
ll_max_level = 10
dwt_level = 10 - 3 = 7

DWT is computed on 1024×1024, and the final 8×8 block is the LL coefficients at decomposition level 7.

Rust immediately downsamples to 8×8 and works only with that matrix. These are different algorithms operating on different information.

2. Different domain for the median

  • Python: numpy.median(dwt_low) — median of wavelet coefficients
  • Rust: median of reconstructed pixels after inverse transform

Even at 8×8, these are not the same.

3. Different wavelet engines

pywt.wavedec2 and dwt::Transform can produce slightly different float values → a different median → different bits.

4. Different resize

Python uses LANCZOS; Rust uses whatever is configured in normalize::proc. Pixel values already differ at the 8×8 stage.


When results may be close

Only if Python is explicitly configured to conditions similar to Rust:

import imagehash
from PIL import Image

h = imagehash.whash(
    Image.open("test.png"),
    hash_size=8,
    image_scale=8,          # same as imgdd
    mode='haar',
    remove_max_haar_ll=True,
)

Even then, exact hex-string matches are not guaranteed because of:

  • different wavelet libraries;
  • Python still hashes coeffs[0] after the second DWT (at dwt_level=0 this is nearly the original 8×8, but not identical to Rust's inverse pixels);
  • different resize filters.

Practical takeaways

Goal Recommendation
Compare with Python imagehash Rewrite Rust whash to follow Python's logic (multi-scale + second DWT + hash LL coeffs)
Use imgdd as-is Compare Rust hashes only with each other (Hamming distance), not with Python
Check similarity Compare Hamming distance within one implementation, not absolute hex values across Python and Rust

Summary

Does the Rust whash implementation match Python imagehash?

No. Rust imgdd whash is not compatible with Python imagehash.whash for cross-language hash comparison. They use different preprocessing scales, different final domains (wavelet coefficients vs reconstructed pixels), and different libraries. Hash differences are expected.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions