code wiki / (root) / nx_phash_index.nx

nx_phash_index.nx source

↩ module page · 84 lines · 3924 B

1// nx_phash_index.nx -- SUBLINEAR Hamming-metric index (BK-tree) over 64-bit perceptual 2// fingerprints. The reverse-image engine (nx_phash / nx_image_search) ranks by a LINEAR O(N) 3// Hamming scan; that is correct but does not scale to a TinEye-class corpus (10^8..10^10 images). 4// This adds a metric tree that, for the small-radius near-duplicate regime, prunes the candidate 5// set to a SUBLINEAR fraction of N via the triangle inequality -- while returning the EXACT same 6// match set as the linear scan (no recall loss). Integer + deterministic end to end (reuses 7// nx_simhash_hamming = popcount64 of XOR; ZERO new metric math). 8// 9// genealogy_id: burkhard_keller_1973_bk_tree (metric tree) over krawetz_2011 dHash fingerprints 10// 11// WHY a BK-tree: Hamming is a true metric (identity, symmetry, triangle inequality), so for any 12// node on the path |d(q,node) - d(node,child)| <= d(q,child). A radius-r query can therefore skip 13// every child whose edge-distance to its parent lies outside [d-r, d+r]. For clustered corpora 14// (real image collections: near-duplicates + visually-similar groups) this prunes the vast 15// majority of nodes. HONEST: on uniform-random codes at large radius it degrades toward linear -- 16// that regime is the Multi-Index-Hashing rung (Norouzi 2012), the next step up. 17// 18// Layout (caller-owned flat arrays, room for `cap` nodes): fp[i], pay[i], and 65 child slots per 19// node at ch[i*65 + d] (d = Hamming edge distance 0..64 inclusive), each = child node index, or 20// -1 = empty. The caller owns/sizes the arrays so the index composes with any storage tier. 21// license_tier: ORIGINAL 22import "nx_simhash.nx" 23 24const PI_SLOTS: i64 = 65 // Hamming edge distances 0..64 inclusive 25 26// set every child slot to -1 (empty). call once before the first insert. 27func pi_init_children(ch: *i64, cap: i64) -> i64 { 28 var i: i64 = 0 29 let total: i64 = cap * PI_SLOTS 30 while i < total { ch[i] = 0 - 1; i = i + 1 } 31 return 0 32} 33 34// insert fingerprint f (payload p) into the BK-tree currently holding n nodes; returns n+1. 35// fp/pay/ch must have room for >= n+1 nodes and ch must be pre-initialised to -1. Terminates: 36// each step either places f in an empty slot or descends to an existing child (finite acyclic tree). 37func pi_insert(fp: *i64, pay: *i64, ch: *i64, n: i64, f: i64, p: i64) -> i64 { 38 if n == 0 { fp[0] = f; pay[0] = p; return 1 } 39 var cur: i64 = 0 40 var placed: i64 = 0 41 while placed == 0 { 42 let d: i64 = nx_simhash_hamming(f, fp[cur]) 43 let slot: i64 = cur * PI_SLOTS + d 44 let c: i64 = ch[slot] 45 if c < 0 { 46 fp[n] = f; pay[n] = p; ch[slot] = n; placed = 1 47 } else { 48 cur = c 49 } 50 } 51 return n + 1 52} 53 54// radius-r search: append the payload of every node within Hamming r of q to out[0..out_cap), 55// return the match count. out_visited[0] is set to the number of fingerprint comparisons made 56// (the candidate set size) -- the measured-exceed counter vs the linear baseline's N. 57func pi_query(fp: *i64, pay: *i64, ch: *i64, n: i64, q: i64, r: i64, out: *i64, out_cap: i64, out_visited: *i64) -> i64 { 58 out_visited[0] = 0 59 if n <= 0 { return 0 } 60 let stack: *i64 = sys_mmap(8 * (n + 8)) as *i64 61 var sp: i64 = 0 62 var nout: i64 = 0 63 var visited: i64 = 0 64 stack[0] = 0; sp = 1 65 while sp > 0 { 66 sp = sp - 1 67 let cur: i64 = stack[sp] 68 let d: i64 = nx_simhash_hamming(q, fp[cur]) 69 visited = visited + 1 70 if d <= r { if nout < out_cap { out[nout] = pay[cur]; nout = nout + 1 } } 71 var lo: i64 = d - r 72 if lo < 0 { lo = 0 } 73 var hi: i64 = d + r 74 if hi > 64 { hi = 64 } 75 var dd: i64 = lo 76 while dd <= hi { 77 let c: i64 = ch[cur * PI_SLOTS + dd] 78 if c >= 0 { stack[sp] = c; sp = sp + 1 } 79 dd = dd + 1 80 } 81 } 82 out_visited[0] = visited 83 return nout 84}