///|
/// Perceptual hashes: compact 64-bit fingerprints that survive resizing,
/// recompression and mild tonal shifts, so visually similar images map to
/// nearby hashes. `average_hash` thresholds an 8x8 luma thumbnail against
/// its mean; `difference_hash` compares horizontal neighbors on a 9x8
/// thumbnail. Similarity is measured with `hamming_distance` (0 = same,
/// 64 = opposite).
///|
/// Mean luma of the source-image region covering thumbnail cell
/// `(cx, cy)` in a `cols` x `rows` grid. Uses exact integer accumulation
/// over the (possibly clipped, always non-empty) pixel block.
fn Image::cell_luma(
self : Image,
cx : Int,
cy : Int,
cols : Int,
rows : Int,
) -> Int {
let x0 = cx * self.width / cols
let x1_raw = (cx + 1) * self.width / cols
let y0 = cy * self.height / rows
let y1_raw = (cy + 1) * self.height / rows
// Guarantee at least one pixel per cell even when the image is smaller
// than the grid.
let x1 = if x1_raw <= x0 { x0 + 1 } else { x1_raw }
let y1 = if y1_raw <= y0 { y0 + 1 } else { y1_raw }
let xe = if x1 > self.width { self.width } else { x1 }
let ye = if y1 > self.height { self.height } else { y1 }
let xs = if x0 >= xe { xe - 1 } else { x0 }
let ys = if y0 >= ye { ye - 1 } else { y0 }
let mut sum = 0
let mut count = 0
for y in ys.. UInt64 {
let cells = FixedArray::make(64, 0)
let mut total = 0
for cy in 0..<8 {
for cx in 0..<8 {
let l = self.cell_luma(cx, cy, 8, 8)
cells[cy * 8 + cx] = l
total = total + l
}
}
let mean = total / 64
let mut hash = 0UL
for i in 0..<64 {
if cells[i] > mean {
hash = hash | (1UL << i)
}
}
hash
}
///|
/// Difference hash (dHash): bit `i = y*8 + x` is set when cell `(x+1, y)`
/// of a 9x8 luma thumbnail is brighter than cell `(x, y)` — a horizontal
/// gradient signature that is highly robust to global tonal changes.
pub fn Image::difference_hash(self : Image) -> UInt64 {
let mut hash = 0UL
for cy in 0..<8 {
for cx in 0..<8 {
let left = self.cell_luma(cx, cy, 9, 8)
let right = self.cell_luma(cx + 1, cy, 9, 8)
if right > left {
hash = hash | (1UL << (cy * 8 + cx))
}
}
}
hash
}
///|
/// Number of differing bits between two 64-bit hashes (0..64). As a rule
/// of thumb for aHash/dHash: <= 5 means "very likely the same image",
/// >= 20 means "clearly different".
pub fn hamming_distance(a : UInt64, b : UInt64) -> Int {
let mut x = a ^ b
let mut count = 0
while x != 0UL {
count = count + 1
x = x & (x - 1UL) // clear the lowest set bit
}
count
}