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.
Hello.
I try to compare hashes, but i have a difference.
I asked Ai.
Results =>
Comparing
whash: Pythonimagehashvs RustimgddShort answer: no, these are not the same implementation. Hashes may match only in narrow edge cases; under normal use they will differ.
Python
imagehash.whashSource: imagehash/init.py
Rust
imgdd::ImageHash::whashSource: hashing.rs#L204
Before calling it, dedupe.rs always resizes the image to 8×8:
Key differences
imagehashimgddimage_scale— largest power of 2 up to image size (512px → 512×512)wavedec2:coeffs[0](8×8 coefficients)pywt)dwt+ HaarANTIALIAS)Lanczos3, etc.)hash_size,image_scale,mode,remove_max_haar_llWhy hashes do not match
1. Different scale (main reason)
For a 1024×1024 image, Python by default does:
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
numpy.median(dwt_low)— median of wavelet coefficientsEven at 8×8, these are not the same.
3. Different wavelet engines
pywt.wavedec2anddwt::Transformcan 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:
Even then, exact hex-string matches are not guaranteed because of:
coeffs[0]after the second DWT (atdwt_level=0this is nearly the original 8×8, but not identical to Rust's inverse pixels);Practical takeaways
imagehashwhashto follow Python's logic (multi-scale + second DWT + hash LL coeffs)imgddas-isSummary
No. Rust
imgddwhashis not compatible with Pythonimagehash.whashfor cross-language hash comparison. They use different preprocessing scales, different final domains (wavelet coefficients vs reconstructed pixels), and different libraries. Hash differences are expected.