code wiki / _hdl_build / nx_struct_diff.nx
nx_struct_diff.nx source
↩ module page · 107 lines · 4666 B
1// nx_struct_diff.nx -- STRUCTURAL exactness differ (the operator ratchet's metric, 2026-07-28):
2// extracts TEXT-LINE BANDS (consecutive inky rows) from two screenshots of the same page region
3// (oracle vs nishi), matches bands by vertical proximity, and reports the numbers OCR cannot see:
4// band counts, matched permille, mean |dy| (vertical drift), mean |dx| (line-start drift). Exact
5// structural match (the bar) = matched 1000 with dy/dx ~ 0; every miss is a nameable layout defect.
6// usage: nx_struct_diff <oracle.png> <nishi.png> <ay> <by> <h>
7import "nx_syscalls.nx"
8import "nx_visual_diff.nx"
9const SD_MAGIC_100000: i64 = 100000
10
11func sdw(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
12func sdn(v: i64) -> i64 { let b: *u8=sys_mmap(28); var m: i64=v; if m<0{sdw("-" as *u8);m=0-m} let t: *u8=sys_mmap(28); var k: i64=0; if m==0{t[0]=48 as u8;k=1} while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1} var i: i64=0; while i<k{b[i]=t[k-1-i];i=i+1} sys_write(1,b,k); return 0 }
13func sd_atoi(s: *u8) -> i64 { var v: i64=0; var i: i64=0; while (s[i]&0xff)>=48 { if (s[i]&0xff)<=57 { v=v*10+((s[i]&0xff)-48); i=i+1 } else { return v } } return v }
14
15const SD_MAXBANDS: i64 = 2048
16// extract line bands of im rows [off, off+h): band = consecutive rows with >=3 ink px
17// (ink = mean rgb < 160, x in [8, w-8)). Writes ymid/xs into out arrays; returns band count.
18func sd_bands(im: *VdImg, off: i64, h: i64, ymid: *i64, xs: *i64) -> i64 {
19 var nb: i64 = 0
20 var in_band: i64 = 0
21 var b_y0: i64 = 0
22 var b_xs: i64 = SD_MAGIC_100000
23 var y: i64 = 0
24 while y < h {
25 let ry: i64 = off + y
26 var ink: i64 = 0
27 var firstx: i64 = SD_MAGIC_100000
28 if ry < im.h {
29 var x: i64 = 8
30 while x < im.w - 8 {
31 let pi: i64 = (ry * im.w + x) * im.nc
32 let lum: i64 = ((im.px[pi]&0xff) + (im.px[pi+1]&0xff) + (im.px[pi+2]&0xff)) / 3
33 if lum < 160 { ink = ink + 1; if x < firstx { firstx = x } }
34 x = x + 2
35 }
36 }
37 if ink >= 3 {
38 if in_band == 0 { in_band = 1; b_y0 = y; b_xs = firstx }
39 else { if firstx < b_xs { b_xs = firstx } }
40 } else {
41 if in_band == 1 {
42 if nb < SD_MAXBANDS { ymid[nb] = (b_y0 + y) / 2; xs[nb] = b_xs; nb = nb + 1 }
43 in_band = 0
44 }
45 }
46 y = y + 1
47 }
48 if in_band == 1 { if nb < SD_MAXBANDS { ymid[nb] = (b_y0 + h) / 2; xs[nb] = b_xs; nb = nb + 1 } }
49 return nb
50}
51
52func main(argc: i64, argv: *i64) -> i64 {
53 if argc < 6 { sdw("usage: nx_struct_diff <oracle.png> <nishi.png> <ay> <by> <h>\n" as *u8); sys_exit(2); return 2 }
54 let a: *VdImg = vd_load(argv[1] as *u8)
55 let b: *VdImg = vd_load(argv[2] as *u8)
56 if (a as i64) == 0 { sdw("STRUCT-DIFF FAIL: cannot load A\n" as *u8); sys_exit(3); return 3 }
57 if (b as i64) == 0 { sdw("STRUCT-DIFF FAIL: cannot load B\n" as *u8); sys_exit(3); return 3 }
58 let ay: i64 = sd_atoi(argv[3] as *u8)
59 let by: i64 = sd_atoi(argv[4] as *u8)
60 let h: i64 = sd_atoi(argv[5] as *u8)
61 let aym: *i64 = sys_mmap(8*SD_MAXBANDS) as *i64
62 let axs: *i64 = sys_mmap(8*SD_MAXBANDS) as *i64
63 let bym: *i64 = sys_mmap(8*SD_MAXBANDS) as *i64
64 let bxs: *i64 = sys_mmap(8*SD_MAXBANDS) as *i64
65 let bused: *i64 = sys_mmap(8*SD_MAXBANDS) as *i64
66 let na: i64 = sd_bands(a, ay, h, aym, axs)
67 let nb2: i64 = sd_bands(b, by, h, bym, bxs)
68 var matched: i64 = 0
69 var sumdy: i64 = 0
70 var sumdx: i64 = 0
71 var i: i64 = 0
72 while i < na {
73 var best: i64 = 0 - 1
74 var bestd: i64 = 26 // match tolerance: 25px vertical
75 var j: i64 = 0
76 while j < nb2 {
77 if bused[j] == 0 {
78 var d: i64 = bym[j] - aym[i]
79 if d < 0 { d = 0 - d }
80 if d < bestd { bestd = d; best = j }
81 }
82 j = j + 1
83 }
84 if best >= 0 {
85 bused[best] = 1
86 matched = matched + 1
87 sumdy = sumdy + bestd
88 var dx: i64 = bxs[best] - axs[i]
89 if dx < 0 { dx = 0 - dx }
90 sumdx = sumdx + dx
91 }
92 i = i + 1
93 }
94 sdw("STRUCT bandsA=" as *u8); sdn(na)
95 sdw(" bandsB=" as *u8); sdn(nb2)
96 var pm: i64 = 0
97 if na > 0 { pm = (matched * 1000) / na }
98 sdw(" matched=" as *u8); sdn(matched)
99 sdw(" (" as *u8); sdn(pm); sdw(" permille)" as *u8)
100 if matched > 0 {
101 sdw(" mean_dy=" as *u8); sdn(sumdy / matched)
102 sdw(" mean_dx=" as *u8); sdn(sumdx / matched)
103 }
104 sdw("\n" as *u8)
105 sys_exit(0)
106 return 0
107}