nx_image_search.nx source
↩ module page · 220 lines · 10328 B
1// nx_image_search.nx -- REUSABLE per-site SEARCH-BY-IMAGE (reverse-image) CLIENT.
2// ONE perceptual-hash engine pointed at ANY site via a data-driven sites registry (Cardinal 11/17).
3// Each site = its OWN durable fingerprint index, so a query is SCOPED to exactly that site BY
4// CONSTRUCTION (site A's index file physically cannot return site B's images). This is the image-side
5// analog of nx_onsite_search (which does text->doc); here we do image->image: "find this image across
6// THIS site's corpus", Yandex/Google/Bing reverse-image style, done sovereignly.
7//
8// Usage: nx_image_search <sites.tsv> <site> <query_gray_path> <w> <h> [threshold]
9// Registry row (TAB-separated, trailing newline required): site<TAB>fp_index_path<TAB>base_url
10// fp_index line (built by nx_image_index / the gate; docid = line number):
11// cid<TAB>dhash_hex16<TAB>ahash_hex16
12//
13// REUSE not reinvent: dHash/aHash + nearest-fingerprint + 64-bit Hamming all come from nx_phash
14// (Krawetz 2011 / Zauner 2010) -> ZERO new ranking math. The query image is a raw grayscale buffer
15// (w*h u8) so the client stays format-agnostic; the real-corpus path (PNG/JPEG decode -> gray) is a
16// separate ingestor rung. dHash ranks (robust to brightness/recompress/resize); aHash breaks ties.
17// HONEST: this is the FAST near-duplicate tier; semantic "looks-like" needs a trained embedding (later
18// rung). license_tier: ORIGINAL
19import "nx_phash.nx"
20import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
21import "nx_image_gray.nx" // shared decode -> gray (same fn the indexer uses, so query+index fingerprint identically)
22
23const IS_TOP_K: i64 = 5
24const IS_DEFAULT_THRESH: i64 = 10
25
26func is_puts(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
27func is_putb(p: *u8, n: i64) -> i64 { if n>0 { sys_write(1,p,n) } return 0 }
28// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
29// per call and never freed it. At page granularity that is 4096B leaked PER CALL -- the
30// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff. A BENCH is the worst home for
31// it: its purpose is millions of iterations. nxi_* is MSB-first and allocates NOTHING.
32func is_num(v: i64) -> i64 { nxi_out(v); return 0 }
33func is_strlen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
34func is_streq(a: *u8, b: *u8) -> i64 { var i: i64=0; while a[i]!=(0 as u8){ if a[i]!=b[i]{return 0} i=i+1 } if b[i]!=(0 as u8){return 0} return 1 }
35func is_atoi(s: *u8) -> i64 { var v: i64=0; var i: i64=0; while s[i]!=(0 as u8){ if s[i]>=(48 as u8){ if s[i]<=(57 as u8){ v=v*10+((s[i]-(48 as u8)) as i64) } } i=i+1 } return v }
36
37// parse 16 lowercase-hex chars at p -> i64 bit pattern (unsigned; 0xff..ff round-trips to -1, Hamming-safe)
38func is_hexparse(p: *u8) -> i64 {
39 var v: i64=0
40 var i: i64=0
41 while i<16 {
42 let c: i64=p[i] as i64
43 var nib: i64=0
44 if c>=48 { if c<=57 { nib=c-48 } }
45 if c>=97 { if c<=102 { nib=c-87 } }
46 v=(v<<4)|nib
47 i=i+1
48 }
49 return v
50}
51
52// registry lookup -- 3 fields site<TAB>fp_index<TAB>base_url. NUL-terminate the row's fields IN PLACE;
53// return field pointers. 1=found, 0=not. Registry must end with a newline.
54func is_lookup_site(reg: *u8, n: i64, site: *u8, out_idx: *i64, out_base: *i64) -> i64 {
55 var ls: i64=0; var i: i64=0
56 while i<=n {
57 var eol: i64=0
58 if i==n { eol=1 } else { if reg[i]==(10 as u8) { eol=1 } }
59 if eol==1 {
60 if i>ls {
61 var t1: i64=0-1; var t2: i64=0-1
62 var k: i64=ls
63 while k<i {
64 if reg[k]==(9 as u8) { if t1<0 { t1=k } else { if t2<0 { t2=k } } }
65 k=k+1
66 }
67 if t1>ls { if t2>t1 {
68 reg[t1]=0 as u8; reg[t2]=0 as u8
69 if i<n { reg[i]=0 as u8 }
70 let sp: *u8=((reg as i64)+ls) as *u8
71 if is_streq(sp, site)==1 {
72 out_idx[0]=(reg as i64)+t1+1
73 out_base[0]=(reg as i64)+t2+1
74 return 1
75 }
76 } }
77 }
78 ls=i+1
79 }
80 i=i+1
81 }
82 return 0
83}
84
85func main(argc: i64, argv: *i64) -> i64 {
86 if argc<4 {
87 is_puts("usage: nx_image_search <sites.tsv> <site> <query_image_path> [w h] [threshold]\n" as *u8)
88 is_puts(" PNG query: w/h auto-detected from the file. raw-gray query: w h required.\n" as *u8)
89 sys_exit(2); return 2
90 }
91 let sites_tsv: *u8=argv[1] as *u8
92 let site: *u8=argv[2] as *u8
93 let qpath: *u8=argv[3] as *u8
94 var w: i64=0
95 var h: i64=0
96 if argc>=6 { w=is_atoi(argv[4] as *u8); h=is_atoi(argv[5] as *u8) }
97 var thresh: i64=IS_DEFAULT_THRESH
98 if argc>=7 { thresh=is_atoi(argv[6] as *u8) }
99
100 // registry + site lookup (per-site isolation: we only ever open THIS site's index)
101 let regbox: *i64=sys_mmap(16) as *i64
102 let reg: *u8=sys_read_file(sites_tsv, regbox)
103 if reg==0 as *u8 { is_puts("IMGSEARCH-FAIL: no sites registry\n" as *u8); sys_exit(1); return 1 }
104 let oidx: *i64=sys_mmap(8) as *i64
105 let obase: *i64=sys_mmap(8) as *i64
106 if is_lookup_site(reg, regbox[0], site, oidx, obase)==0 {
107 is_puts("IMGSEARCH-FAIL: unknown site '" as *u8); is_puts(site); is_puts("' (not in registry)\n" as *u8)
108 sys_exit(1); return 1
109 }
110 let idx_path: *u8=oidx[0] as *u8
111 let base_url: *u8=obase[0] as *u8
112
113 // read the query image and perceptual-hash it. AUTO-DETECT: a PNG (magic 89 50 4E 47) is decoded
114 // via the shared nx_img_to_gray (same path the indexer used); anything else is treated as a raw
115 // grayscale w*h buffer (needs w,h). This is the real "search by image": hand it an actual image.
116 let qbox: *i64=sys_mmap(16) as *i64
117 let qbuf: *u8=sys_read_file(qpath, qbox)
118 if qbuf==0 as *u8 { is_puts("IMGSEARCH-FAIL: cannot read query image\n" as *u8); sys_exit(1); return 1 }
119 var gray: *u8=qbuf
120 var gw: i64=w
121 var gh: i64=h
122 var ispng: i64=0
123 if qbox[0]>=8 { if qbuf[0]==(137 as u8) { if qbuf[1]==(80 as u8) { if qbuf[2]==(78 as u8) { if qbuf[3]==(71 as u8) { ispng=1 } } } } }
124 if ispng==1 {
125 let wh: *i64=sys_mmap(16) as *i64
126 let g: *u8=nx_img_to_gray(qpath, wh)
127 if g==0 as *u8 { is_puts("IMGSEARCH-FAIL: cannot decode PNG query (unsupported/corrupt)\n" as *u8); sys_exit(1); return 1 }
128 gray=g; gw=wh[0]; gh=wh[1]
129 } else {
130 if gw<=0 { is_puts("IMGSEARCH-FAIL: raw-gray query needs <w> <h> args\n" as *u8); sys_exit(1); return 1 }
131 if gh<=0 { is_puts("IMGSEARCH-FAIL: raw-gray query needs <w> <h> args\n" as *u8); sys_exit(1); return 1 }
132 if qbox[0]<gw*gh { is_puts("IMGSEARCH-FAIL: raw-gray query smaller than w*h\n" as *u8); sys_exit(1); return 1 }
133 }
134 let qd: i64=nx_phash_dhash(gray, gw, gh)
135 let qa: i64=nx_phash_ahash(gray, gw, gh)
136
137 // load this site's fingerprint index (one read): lines cid<TAB>dhash_hex<TAB>ahash_hex
138 let mbox: *i64=sys_mmap(16) as *i64
139 let mbuf: *u8=sys_read_file(idx_path, mbox)
140 if mbuf==0 as *u8 { is_puts("IMGSEARCH-FAIL: no index for site (run nx_image_index first)\n" as *u8); sys_exit(1); return 1 }
141 let mn: i64=mbox[0]
142 var nlines: i64=0; var ii: i64=0
143 while ii<mn { if mbuf[ii]==(10 as u8) { nlines=nlines+1 } ii=ii+1 }
144 if nlines<=0 { is_puts("IMGSEARCH-FAIL: empty index\n" as *u8); sys_exit(1); return 1 }
145 let cidp: *i64=sys_mmap(8*(nlines+8)) as *i64
146 let cidl: *i64=sys_mmap(8*(nlines+8)) as *i64
147 let dh: *i64=sys_mmap(8*(nlines+8)) as *i64
148 let ah: *i64=sys_mmap(8*(nlines+8)) as *i64
149 var ndocs: i64=0; var ls: i64=0
150 ii=0
151 while ii<mn {
152 if mbuf[ii]==(10 as u8) {
153 let le: i64=ii
154 var t1: i64=0-1; var t2: i64=0-1
155 var k: i64=ls
156 while k<le { if mbuf[k]==(9 as u8) { if t1<0 { t1=k } else { if t2<0 { t2=k } } } k=k+1 }
157 if t1>ls { if t2>t1 {
158 cidp[ndocs]=(mbuf as i64)+ls; cidl[ndocs]=t1-ls
159 dh[ndocs]=is_hexparse(((mbuf as i64)+t1+1) as *u8)
160 ah[ndocs]=is_hexparse(((mbuf as i64)+t2+1) as *u8)
161 ndocs=ndocs+1
162 } }
163 ls=ii+1
164 }
165 ii=ii+1
166 }
167 if ndocs<=0 { is_puts("IMGSEARCH-FAIL: index parse yielded 0 rows\n" as *u8); sys_exit(1); return 1 }
168
169 // per-doc Hamming (dHash primary, aHash tiebreak); selection-sort an index permutation ascending
170 let order: *i64=sys_mmap(8*(ndocs+8)) as *i64
171 let hamd: *i64=sys_mmap(8*(ndocs+8)) as *i64
172 let hama: *i64=sys_mmap(8*(ndocs+8)) as *i64
173 var d: i64=0
174 while d<ndocs {
175 order[d]=d
176 hamd[d]=nx_simhash_hamming(qd, dh[d])
177 hama[d]=nx_simhash_hamming(qa, ah[d])
178 d=d+1
179 }
180 var a: i64=0
181 while a<ndocs {
182 var best: i64=a; var bz: i64=a+1
183 while bz<ndocs {
184 let cb: i64=order[bz]; let cbest: i64=order[best]
185 var better: i64=0
186 if hamd[cb]<hamd[cbest] { better=1 } else { if hamd[cb]==hamd[cbest] { if hama[cb]<hama[cbest] { better=1 } } }
187 if better==1 { best=bz }
188 bz=bz+1
189 }
190 if best!=a { let tmp: i64=order[a]; order[a]=order[best]; order[best]=tmp }
191 a=a+1
192 }
193
194 // matches = images within the dHash threshold ("near this image")
195 var nm: i64=0
196 d=0
197 while d<ndocs { if hamd[d]<=thresh { nm=nm+1 } d=d+1 }
198
199 is_puts("IMGSEARCH-RESULTS site=" as *u8); is_puts(site)
200 is_puts(" indexed=" as *u8); is_num(ndocs)
201 is_puts(" matches=" as *u8); is_num(nm); is_puts("\n" as *u8)
202 if nm==0 {
203 is_puts("IMGSEARCH-OK matches=0 (no indexed image within threshold)\n" as *u8)
204 sys_exit(0); return 0
205 }
206 var topk: i64=nm
207 if topk>IS_TOP_K { topk=IS_TOP_K }
208 var r: i64=0
209 while r<topk {
210 let di: i64=order[r]
211 is_puts(" #" as *u8); is_num(r+1)
212 is_puts(" ham=" as *u8); is_num(hamd[di])
213 is_puts(" simq16=" as *u8); is_num(nx_phash_similarity_q16(qd, dh[di]))
214 is_puts(" " as *u8); is_puts(base_url); is_putb(cidp[di] as *u8, cidl[di])
215 is_puts("\n" as *u8)
216 r=r+1
217 }
218 is_puts("IMGSEARCH-OK\n" as *u8)
219 sys_exit(0); return 0
220}