nx_imgsearch_engine.nx source
↩ module page · 476 lines · 22067 B
1// nx_imgsearch_engine.nx -- THE MULTI-TIER REVERSE-IMAGE QUERY ENGINE.
2//
3// Replaces the shipped single-tier client (nx_image_search) on two counts, both of them load-bearing:
4//
5// 1. IT SCALES. The shipped client ranks by scanning every fingerprint and then SELECTION-SORTING the
6// whole corpus -- O(N^2). At 1,000 images that is a million comparisons; at the 10^6 an indie image
7// index reaches it is 10^12 and the query never returns. Here every tier extracts its top-K through
8// a BOUNDED MAX-HEAP of size K: O(N log K) time and O(K) space, with N touched exactly once. Going
9// from 10^3 to 10^6 images multiplies the work by 10^3, not 10^6.
10//
11// 2. IT FUSES. One descriptor cannot cover the modification space (nx_imgbench proves it class by
12// class), so the engine runs every registered tier and combines their ranked lists with RECIPROCAL
13// RANK FUSION -- score = sum over tiers of weight / (K_RRF + rank). RRF is the fusion the S-Class IR
14// literature settles on precisely because it needs no score calibration between rankers: a Hamming
15// distance over 64 bits and an L1 distance over 80 dimensions are never comparable as magnitudes,
16// but their RANKS always are. Same integer discipline as the text engine -- no floats anywhere.
17//
18// The engine holds only base-class tier pointers. Adding local-feature or semantic matching is a new
19// nx_imgtier subtype plus one add_tier call; this file does not change. That is the design contract.
20//
21// genealogy_id: cormack_2009_reciprocal_rank_fusion + williams_1975_bounded_heap_selection
22// license_tier: ORIGINAL
23import "nx_imgsearch_tier.nx"
24import "nx_phash_index.nx"
25const NX_MAGIC_1000000: i64 = 1000000
26
27const NX_IE_MAXTIERS: i64 = 8
28const NX_IE_MAXK: i64 = 64
29const NX_IE_RRF_K: i64 = 60 // Cormack/Clarke/Buettcher 2009 -- damps the top-rank monopoly
30
31// Per-tier Hamming ACCELERATION. A hash tier (a single 64-bit fingerprint per image) can generate its
32// within-threshold candidates SUBLINEARLY via a BK-tree (nx_phash_index) instead of scanning all N.
33// The BK-tree returns the EXACT within-radius set by the triangle inequality -- no recall loss -- while
34// touching a small fraction of the corpus on clustered data. Without this, a 10M-image index is a
35// 10M-comparison scan PER TIER PER QUERY; with it, candidate generation is what actually scales. Non-
36// hash tiers (the 80-dim similarity tier) leave active=0 and fall back to the linear heap scan (their
37// scale path is an ANN graph -- nx_vec_nsw, filed seq717).
38struct nx_bkaccel {
39 active: i64, // 1 if this tier is BK-accelerated
40 fp: i64, // *i64 : node fingerprints
41 pay: i64, // *i64 : node payloads (image indices)
42 ch: i64, // *i64 : 65 child slots per node
43 n: i64, // node count
44}
45const NX_BKACCEL_BYTES: i64 = 40
46
47struct nx_imgengine {
48 ntiers: i64,
49 tiers: i64, // *i64 : tier pointers
50 cap: i64,
51 count: i64,
52 descs: i64, // *i64 : per-tier descriptor block pointers
53 pays: i64, // *i64 : caller payload (docid / cid) per indexed image
54 accel: i64, // *i64 : per-tier *nx_bkaccel (0 = not accelerated)
55 last_visited: i64, // BK comparisons made by the most recent accelerated tier_topk (for the ruler)
56}
57
58const NX_IMGENGINE_BYTES: i64 = 64
59
60func nx_imgengine_count(e: *nx_imgengine) -> i64 { return e.count }
61func nx_imgengine_cap(e: *nx_imgengine) -> i64 { return e.cap }
62func nx_imgengine_ntiers(e: *nx_imgengine) -> i64 { return e.ntiers }
63func nx_imgengine_last_visited(e: *nx_imgengine) -> i64 { return e.last_visited }
64func nx_imgengine_tier(e: *nx_imgengine, i: i64) -> *nx_imgtier {
65 let arr: *i64 = e.tiers as *i64
66 return arr[i] as *nx_imgtier
67}
68
69func nx_imgengine_new(cap: i64) -> *nx_imgengine {
70 let e: *nx_imgengine = sys_mmap(NX_IMGENGINE_BYTES) as *nx_imgengine
71 e.ntiers = 0
72 e.tiers = sys_mmap(8 * NX_IE_MAXTIERS) as i64
73 e.cap = cap
74 e.count = 0
75 e.descs = sys_mmap(8 * NX_IE_MAXTIERS) as i64
76 e.pays = sys_mmap(8 * cap) as i64
77 e.accel = sys_mmap(8 * NX_IE_MAXTIERS) as i64
78 var z: i64 = 0
79 let aarr: *i64 = e.accel as *i64
80 while z < NX_IE_MAXTIERS { aarr[z] = 0; z = z + 1 }
81 e.last_visited = 0
82 return e
83}
84
85func ie_accel(e: *nx_imgengine, ti: i64) -> *nx_bkaccel {
86 let aarr: *i64 = e.accel as *i64
87 return aarr[ti] as *nx_bkaccel
88}
89
90// a hash tier stores exactly one 64-bit fingerprint per image (idx_dim==1) and ranks by Hamming, so
91// the BK-tree metric index applies. That is the copy and orientation tiers.
92func ie_tier_is_hash(t: *nx_imgtier) -> i64 {
93 if nx_imgtier_idx_dim(t) == 1 {
94 if nx_imgtier_kind(t) == NX_IT_KIND_COPY { return 1 }
95 if nx_imgtier_kind(t) == NX_IT_KIND_ORIENT { return 1 }
96 }
97 return 0
98}
99
100// register a tier. Its descriptor block is allocated here from the tier's own declared idx_dim, so a
101// new subtype never needs the engine to know its width. A hash tier also gets a BK-tree. Returns the
102// tier slot, or -1 if full.
103func nx_imgengine_add_tier(e: *nx_imgengine, t: *nx_imgtier) -> i64 {
104 if e.ntiers >= NX_IE_MAXTIERS { return 0 - 1 }
105 let slot: i64 = e.ntiers
106 let tarr: *i64 = e.tiers as *i64
107 let darr: *i64 = e.descs as *i64
108 let aarr: *i64 = e.accel as *i64
109 tarr[slot] = t as i64
110 darr[slot] = sys_mmap(8 * e.cap * nx_imgtier_idx_dim(t)) as i64
111 if ie_tier_is_hash(t) == 1 {
112 let ac: *nx_bkaccel = sys_mmap(NX_BKACCEL_BYTES) as *nx_bkaccel
113 ac.active = 1
114 ac.fp = sys_mmap(8 * e.cap) as i64
115 ac.pay = sys_mmap(8 * e.cap) as i64
116 ac.ch = sys_mmap(8 * e.cap * PI_SLOTS) as i64
117 pi_init_children(ac.ch as *i64, e.cap)
118 ac.n = 0
119 aarr[slot] = ac as i64
120 } else {
121 aarr[slot] = 0
122 }
123 e.ntiers = slot + 1
124 return slot
125}
126
127// descriptor slot of image `i` for tier `ti`
128func ie_desc_at(e: *nx_imgengine, ti: i64, i: i64) -> *i64 {
129 let darr: *i64 = e.descs as *i64
130 let t: *nx_imgtier = nx_imgengine_tier(e, ti)
131 let d: i64 = nx_imgtier_idx_dim(t)
132 return (darr[ti] + 8 * i * d) as *i64
133}
134
135// INGEST: describe one image with every registered tier and store it. rgb may be 0 for tiers that
136// only need luminance. Returns the new corpus count, unchanged if the engine is full (bounded, never
137// a silent overwrite).
138func nx_imgengine_add_image(e: *nx_imgengine, gray: *u8, rgb: *u8, w: i64, h: i64, payload: i64) -> i64 {
139 if e.count >= e.cap { return e.count }
140 let i: i64 = e.count
141 var ti: i64 = 0
142 while ti < e.ntiers {
143 let t: *nx_imgtier = nx_imgengine_tier(e, ti)
144 let slot: *i64 = ie_desc_at(e, ti, i)
145 nx_imgtier_describe_index(t, gray, rgb, w, h, slot)
146 // mirror a hash tier's fingerprint into its BK-tree (payload = image index, so a radius query
147 // returns image indices directly). Incremental insert -- no separate build phase.
148 let ac: *nx_bkaccel = ie_accel(e, ti)
149 if ac != (0 as *nx_bkaccel) { if ac.active == 1 {
150 ac.n = pi_insert(ac.fp as *i64, ac.pay as *i64, ac.ch as *i64, ac.n, slot[0], i)
151 } }
152 ti = ti + 1
153 }
154 let parr: *i64 = e.pays as *i64
155 parr[i] = payload
156 e.count = i + 1
157 return e.count
158}
159
160// ---- bounded max-heap: keeps the K SMALLEST keys seen, in O(log K) per candidate ----
161func ie_swap(hd: *i64, hp: *i64, a: i64, b: i64) -> i64 {
162 let td: i64 = hd[a]; hd[a] = hd[b]; hd[b] = td
163 let tp: i64 = hp[a]; hp[a] = hp[b]; hp[b] = tp
164 return 0
165}
166func ie_heap_up(hd: *i64, hp: *i64, start: i64) -> i64 {
167 var i: i64 = start
168 var go: i64 = 1
169 while go == 1 {
170 if i <= 0 { go = 0 } else {
171 let p: i64 = (i - 1) / 2
172 if hd[p] < hd[i] { ie_swap(hd, hp, p, i); i = p } else { go = 0 }
173 }
174 }
175 return 0
176}
177func ie_heap_down(hd: *i64, hp: *i64, n: i64, start: i64) -> i64 {
178 var i: i64 = start
179 var go: i64 = 1
180 while go == 1 {
181 let l: i64 = 2 * i + 1
182 let r: i64 = l + 1
183 var m: i64 = i
184 if l < n { if hd[l] > hd[m] { m = l } }
185 if r < n { if hd[r] > hd[m] { m = r } }
186 if m == i { go = 0 } else { ie_swap(hd, hp, m, i); i = m }
187 }
188 return 0
189}
190// offer candidate (key,item); nbox[0] holds the current heap size. Root is always the WORST kept.
191func ie_heap_offer(hd: *i64, hp: *i64, nbox: *i64, k: i64, key: i64, item: i64) -> i64 {
192 let n: i64 = nbox[0]
193 if n < k {
194 hd[n] = key; hp[n] = item
195 nbox[0] = n + 1
196 ie_heap_up(hd, hp, n)
197 return 1
198 }
199 if key < hd[0] {
200 hd[0] = key; hp[0] = item
201 ie_heap_down(hd, hp, n, 0)
202 return 1
203 }
204 return 0
205}
206// drain the heap into out_* sorted ASCENDING by key (pop-max fills from the back). Returns the count.
207func ie_heap_drain(hd: *i64, hp: *i64, n_in: i64, out_key: *i64, out_item: *i64) -> i64 {
208 var n: i64 = n_in
209 var w: i64 = n_in - 1
210 while n > 0 {
211 out_key[w] = hd[0]
212 out_item[w] = hp[0]
213 n = n - 1
214 hd[0] = hd[n]; hp[0] = hp[n]
215 ie_heap_down(hd, hp, n, 0)
216 w = w - 1
217 }
218 return n_in
219}
220
221// ACCELERATED hash-tier candidate generation: query the BK-tree at radius = tier threshold for each
222// of the query's fingerprints (1 for copy, 8 for orient), UNION the returned image indices (dedup via
223// a seen bitmap), then rank ONLY those candidates with the bounded heap. Returns the within-threshold
224// set nearest-first -- EXACTLY the set a linear scan would flag as confident, by the BK-tree's
225// triangle-inequality exactness, but touching a sublinear fraction of the corpus. e.last_visited
226// records the comparisons made (the ruler prints it against N). A hash tier legitimately returns only
227// within-threshold items: it has no business ranking images it considers non-matches -- the similarity
228// tier owns the "looks like" fallback when nothing is confident.
229func ie_tier_topk_bk(e: *nx_imgengine, ti: i64, qdesc: *i64, k: i64,
230 out_item: *i64, out_dist: *i64) -> i64 {
231 let t: *nx_imgtier = nx_imgengine_tier(e, ti)
232 let ac: *nx_bkaccel = ie_accel(e, ti)
233 let radius: i64 = nx_imgtier_threshold(t)
234 let nq: i64 = nx_imgtier_qry_dim(t) // 1 (copy) or 8 (orient)
235
236 var kk: i64 = k
237 if kk > NX_IE_MAXK { kk = NX_IE_MAXK }
238 if kk < 1 { kk = 1 }
239 let hd: *i64 = sys_mmap(8 * kk) as *i64
240 let hp: *i64 = sys_mmap(8 * kk) as *i64
241 let nbox: *i64 = sys_mmap(8) as *i64
242 nbox[0] = 0
243
244 let seen: *u8 = sys_mmap(e.count + 8)
245 var z: i64 = 0
246 while z < e.count { seen[z] = 0 as u8; z = z + 1 }
247 let cbuf: *i64 = sys_mmap(8 * (e.count + 8)) as *i64
248 let visbox: *i64 = sys_mmap(8) as *i64
249 var visited: i64 = 0
250
251 var qh: i64 = 0
252 while qh < nq {
253 let nc: i64 = pi_query(ac.fp as *i64, ac.pay as *i64, ac.ch as *i64, ac.n,
254 qdesc[qh], radius, cbuf, e.count, visbox)
255 visited = visited + visbox[0]
256 var c: i64 = 0
257 while c < nc {
258 let img: i64 = cbuf[c]
259 if seen[img] == (0 as u8) {
260 seen[img] = 1 as u8
261 // full tier distance (min over the query's orientations for orient) on the candidate
262 let d: i64 = nx_imgtier_distance(t, qdesc, ie_desc_at(e, ti, img))
263 ie_heap_offer(hd, hp, nbox, kk, d, img)
264 }
265 c = c + 1
266 }
267 qh = qh + 1
268 }
269 e.last_visited = visited
270 return ie_heap_drain(hd, hp, nbox[0], out_dist, out_item)
271}
272
273// TOP-K FOR ONE TIER: BK-accelerated for hash tiers, linear heap scan otherwise. out_item/out_dist
274// ascending by distance. Turns the per-tier scan from O(N) into sublinear candidate generation for the
275// tiers that dominate a reverse-image query.
276func nx_imgengine_tier_topk(e: *nx_imgengine, ti: i64, qdesc: *i64, k: i64,
277 out_item: *i64, out_dist: *i64) -> i64 {
278 let ac: *nx_bkaccel = ie_accel(e, ti)
279 if ac != (0 as *nx_bkaccel) { if ac.active == 1 {
280 return ie_tier_topk_bk(e, ti, qdesc, k, out_item, out_dist)
281 } }
282 var kk: i64 = k
283 if kk > NX_IE_MAXK { kk = NX_IE_MAXK }
284 if kk < 1 { kk = 1 }
285 let hd: *i64 = sys_mmap(8 * kk) as *i64
286 let hp: *i64 = sys_mmap(8 * kk) as *i64
287 let nbox: *i64 = sys_mmap(8) as *i64
288 nbox[0] = 0
289 let t: *nx_imgtier = nx_imgengine_tier(e, ti)
290 e.last_visited = e.count
291 var i: i64 = 0
292 while i < e.count {
293 let d: i64 = nx_imgtier_distance(t, qdesc, ie_desc_at(e, ti, i))
294 ie_heap_offer(hd, hp, nbox, kk, d, i)
295 i = i + 1
296 }
297 return ie_heap_drain(hd, hp, nbox[0], out_dist, out_item)
298}
299
300// ---- THE QUERY: describe once per tier, top-K per tier, fuse by RRF ----
301// out_pay -- caller payload of each result
302// out_score-- fused RRF score (larger is better; arbitrary integer units, comparable within one query)
303// out_tier -- which tier ranked this result best (so the caller can say HOW it matched)
304// out_dist -- that tier's raw distance to the result (Hamming for hash tiers, L1 for descriptor tiers)
305// Returns the number of results written (<= topk).
306// klass_filter: 0 = every registered tier votes; otherwise only tiers of that NX_IT_CLASS_* do.
307// Exists because "is this the same image?" and "what does this look like?" are different questions
308// and the tiers qualified to answer them are different sets (see nx_imgsearch_tier.nx).
309func nx_imgengine_query_class(e: *nx_imgengine, gray: *u8, rgb: *u8, w: i64, h: i64, topk: i64,
310 klass_filter: i64,
311 out_pay: *i64, out_score: *i64, out_tier: *i64, out_dist: *i64) -> i64 {
312 if e.count <= 0 { return 0 }
313 if e.ntiers <= 0 { return 0 }
314 var k: i64 = topk
315 if k > NX_IE_MAXK { k = NX_IE_MAXK }
316 if k < 1 { k = 1 }
317
318 // fused candidate pool: at most one entry per (tier, rank)
319 let pool: i64 = NX_IE_MAXTIERS * NX_IE_MAXK
320 let cand_item: *i64 = sys_mmap(8 * pool) as *i64
321 let cand_score: *i64 = sys_mmap(8 * pool) as *i64 // CONFIDENT score: tiers voting within threshold
322 let cand_raw: *i64 = sys_mmap(8 * pool) as *i64 // RAW score: every tier, threshold ignored
323 let cand_tier: *i64 = sys_mmap(8 * pool) as *i64
324 let cand_dist: *i64 = sys_mmap(8 * pool) as *i64
325 let cand_best: *i64 = sys_mmap(8 * pool) as *i64 // best normalised tier score, breaks RRF ties
326 var ncand: i64 = 0
327
328 let ti_item: *i64 = sys_mmap(8 * NX_IE_MAXK) as *i64
329 let ti_dist: *i64 = sys_mmap(8 * NX_IE_MAXK) as *i64
330
331 var ti: i64 = 0
332 while ti < e.ntiers {
333 let t: *nx_imgtier = nx_imgengine_tier(e, ti)
334 var eligible: i64 = 1
335 if klass_filter != 0 { if nx_imgtier_klass(t) != klass_filter { eligible = 0 } }
336 if eligible == 1 {
337 let qd: *i64 = sys_mmap(8 * nx_imgtier_qry_dim(t)) as *i64
338 nx_imgtier_describe_query(t, gray, rgb, w, h, qd)
339 // A tier also abstains when its OWN descriptor of the query is meaningless. Same principle
340 // as the threshold abstention above, applied one level earlier: a fingerprint with no
341 // structure in it is within distance 0 of every other structureless fingerprint, so its
342 // "match" is an artefact. It may still contribute to the RAW ordering (a caller asking
343 // "what does this most look like?" still gets an answer) but never to CONFIDENCE.
344 let qinfo: i64 = nx_imgtier_informative(t, qd)
345 let n: i64 = nx_imgengine_tier_topk(e, ti, qd, k, ti_item, ti_dist)
346
347 var r: i64 = 0
348 while r < n {
349 let item: i64 = ti_item[r]
350 let dist: i64 = ti_dist[r]
351 let contrib: i64 = nx_imgtier_weight(t) * 1000 / (NX_IE_RRF_K + r + 1)
352 let tscore: i64 = nx_imgtier_score(t, dist)
353 // ABSTENTION -- the correction that makes multi-tier fusion actually work.
354 // Plain RRF assumes every ranker is competent on every query. Ours are not, BY DESIGN:
355 // a hash tier is structurally blind to cropping, a layout tier is structurally blind to
356 // mirroring. Worse, the hash tiers are CORRELATED -- they share a descriptor, so when
357 // they are blind they are blind IDENTICALLY and their two wrong votes out-number the one
358 // tier that is right. Measured: fused crop-20 recall collapsed to 83 permille while the
359 // competent tier alone scored 937. So a tier's vote only counts as CONFIDENT when the
360 // candidate is inside that tier's own declared threshold -- i.e. when the tier is willing
361 // to call it a match. Out-of-competence tiers still contribute to the RAW score, which
362 // orders results only when nothing is confident (graceful degradation: "no match, but
363 // here is what it most looks like" rather than an empty answer).
364 var conf: i64 = 0
365 if qinfo == 1 { if dist <= nx_imgtier_threshold(t) { conf = contrib } }
366 var found: i64 = 0 - 1
367 var c: i64 = 0
368 while c < ncand { if cand_item[c] == item { found = c; c = ncand } else { c = c + 1 } }
369 if found >= 0 {
370 cand_score[found] = cand_score[found] + conf
371 cand_raw[found] = cand_raw[found] + contrib
372 if tscore > cand_best[found] {
373 cand_best[found] = tscore
374 cand_tier[found] = ti
375 cand_dist[found] = dist
376 }
377 } else {
378 if ncand < pool {
379 cand_item[ncand] = item
380 cand_score[ncand] = conf
381 cand_raw[ncand] = contrib
382 cand_tier[ncand] = ti
383 cand_dist[ncand] = dist
384 cand_best[ncand] = tscore
385 ncand = ncand + 1
386 }
387 }
388 r = r + 1
389 }
390 }
391 ti = ti + 1
392 }
393
394 // final top-K over the fused pool. Same bounded heap, keyed by NEGATED score so "smallest key"
395 // means "highest fused score" -- one heap implementation, no second comparator to keep in sync.
396 let hd: *i64 = sys_mmap(8 * k) as *i64
397 let hp: *i64 = sys_mmap(8 * k) as *i64
398 let nbox: *i64 = sys_mmap(8) as *i64
399 nbox[0] = 0
400 // Lexicographic key: CONFIDENT score dominates, RAW score breaks ties. Packing them into one
401 // integer keeps a single heap and a single comparator -- a second comparator is a second thing to
402 // keep in sync. Bounds are safe: contribution <= 1000*1000/61 ~ 16393 per tier, <= 8 tiers, so
403 // each component is < 2^17 and the packed key is < 2^38.
404 var c2: i64 = 0
405 while c2 < ncand { ie_heap_offer(hd, hp, nbox, k, 0 - (cand_score[c2] * NX_MAGIC_1000000 + cand_raw[c2]), c2); c2 = c2 + 1 }
406 let ok: *i64 = sys_mmap(8 * k) as *i64
407 let oi: *i64 = sys_mmap(8 * k) as *i64
408 let nres: i64 = ie_heap_drain(hd, hp, nbox[0], ok, oi)
409
410 let parr: *i64 = e.pays as *i64
411 var i: i64 = 0
412 while i < nres {
413 let c: i64 = oi[i]
414 out_pay[i] = parr[cand_item[c]]
415 out_score[i] = cand_score[c]
416 out_tier[i] = cand_tier[c]
417 out_dist[i] = cand_dist[c]
418 i = i + 1
419 }
420 return nres
421}
422
423// Full reverse-image search: every tier votes. This is the "show me results" call.
424func nx_imgengine_query(e: *nx_imgengine, gray: *u8, rgb: *u8, w: i64, h: i64, topk: i64,
425 out_pay: *i64, out_score: *i64, out_tier: *i64, out_dist: *i64) -> i64 {
426 return nx_imgengine_query_class(e, gray, rgb, w, h, topk, 0, out_pay, out_score, out_tier, out_dist)
427}
428
429// THE IDENTITY QUESTION: "is this exact image (or a modified copy of it) already in the corpus?"
430// Dedup, provenance, copyright. Only IDENTITY-class tiers vote -- a similarity tier's whole purpose
431// is returning pictures that are NOT the query, so its opinion here would be a confident lie.
432// Returns the payload, or -1 when no identity tier is within its own threshold: an honest ABSENT,
433// never the nearest thing anyway.
434// out_reason: 0 = matched, 1 = nothing within threshold, 2 = query carries too little visual
435// information for ANY identity tier to be believed (flat / smooth-gradient / near-blank image).
436// Reason 2 exists because it was found on real data: structureless images all hash identically and
437// would otherwise be reported as confident matches for one another.
438func nx_imgengine_best_match_r(e: *nx_imgengine, gray: *u8, rgb: *u8, w: i64, h: i64,
439 out_tier: *i64, out_dist: *i64, out_reason: *i64) -> i64 {
440 out_tier[0] = 0 - 1
441 out_dist[0] = 0 - 1
442
443 // INFORMATIVENESS GATE, before any ranking: if no identity tier considers the query's own
444 // descriptor meaningful, there is nothing to be confident about and we say so.
445 var informative: i64 = 0
446 var ti: i64 = 0
447 while ti < e.ntiers {
448 let t: *nx_imgtier = nx_imgengine_tier(e, ti)
449 if nx_imgtier_klass(t) == NX_IT_CLASS_IDENTITY {
450 let qd: *i64 = sys_mmap(8 * nx_imgtier_qry_dim(t)) as *i64
451 nx_imgtier_describe_query(t, gray, rgb, w, h, qd)
452 if nx_imgtier_informative(t, qd) == 1 { informative = 1 }
453 }
454 ti = ti + 1
455 }
456 if informative == 0 { out_reason[0] = 2; return 0 - 1 }
457
458 let p: *i64 = sys_mmap(8) as *i64
459 let s: *i64 = sys_mmap(8) as *i64
460 let t2: *i64 = sys_mmap(8) as *i64
461 let d: *i64 = sys_mmap(8) as *i64
462 let n: i64 = nx_imgengine_query_class(e, gray, rgb, w, h, 1, NX_IT_CLASS_IDENTITY, p, s, t2, d)
463 if n <= 0 { out_reason[0] = 1; return 0 - 1 }
464 let tier: *nx_imgtier = nx_imgengine_tier(e, t2[0])
465 out_tier[0] = t2[0]
466 out_dist[0] = d[0]
467 if d[0] <= nx_imgtier_threshold(tier) { out_reason[0] = 0; return p[0] }
468 out_reason[0] = 1
469 return 0 - 1
470}
471
472func nx_imgengine_best_match(e: *nx_imgengine, gray: *u8, rgb: *u8, w: i64, h: i64,
473 out_tier: *i64, out_dist: *i64) -> i64 {
474 let r: *i64 = sys_mmap(8) as *i64
475 return nx_imgengine_best_match_r(e, gray, rgb, w, h, out_tier, out_dist, r)
476}