nx_doc_scan.nx source
↩ module page · 139 lines · 5473 B
1// nx_doc_scan.nx -- R4 of THE NISHI DOCUMENT-INTELLIGENCE arc: the SCAN / CAPTURE leg (paper/image ->
2// characters). The honest first rung of OCR, built sovereign rung-up (NOT a waved "general OCR"):
3// (1) DECODE a standard scanned-image format -- NetPBM P1 (ASCII bitmap, "P1\n<w> <h>\n<0/1 grid>") ->
4// a pixel grid. A real image-file decoder.
5// (2) RECOGNIZE digits by TEMPLATE MATCHING (the classic fixed-font OCR technique): each glyph cell is
6// compared against a sovereign 3x5 reference font (DATA), nearest by Hamming distance wins; the
7// distance IS the confidence (a blank/garbage cell matches at a large distance = low confidence).
8// This turns a scanned bitmap (e.g. an account number / amount) into a structured digit string. Honest
9// scope: fixed 3x5 digit font + fixed-pitch cells; variable fonts / proportional layout / full alphabets
10// are later rungs grounded on the doc_scan_* / doc_read_layout corpus. No hardware writes, no floats.
11// license_tier: ORIGINAL
12import "nx_syscalls.nx"
13const K_MAGIC_9999: i64 = 9999
14
15// the sovereign 3x5 digit font: each glyph is a 15-char row-major "0/1" template (rows top->bottom).
16func sp_pat(d: i64) -> *u8 {
17 if d == 0 { return "111101101101111\x00" as *u8 }
18 if d == 1 { return "010110010010111\x00" as *u8 }
19 if d == 2 { return "111001111100111\x00" as *u8 }
20 if d == 3 { return "111001111001111\x00" as *u8 }
21 if d == 4 { return "101101111001001\x00" as *u8 }
22 if d == 5 { return "111100111001111\x00" as *u8 }
23 if d == 6 { return "111100111101111\x00" as *u8 }
24 if d == 7 { return "111001010010010\x00" as *u8 }
25 if d == 8 { return "111101111101111\x00" as *u8 }
26 if d == 9 { return "111101111001111\x00" as *u8 }
27 return "000000000000000\x00" as *u8
28}
29
30// stamp digit d's 3x5 glyph into the grid at top-left (x0, 0). grid is row-major gw-wide, values 0/1.
31func sp_stamp(grid: *u8, gw: i64, x0: i64, d: i64) -> i64 {
32 let p: *u8 = sp_pat(d)
33 var y: i64 = 0
34 while y < 5 {
35 var x: i64 = 0
36 while x < 3 {
37 grid[y * gw + x0 + x] = (p[y * 3 + x] - 0x30) as u8
38 x = x + 1
39 }
40 y = y + 1
41 }
42 return 0
43}
44
45// Hamming distance between the 3x5 cell at (x0,y0) and digit d's template.
46func sp_celldist(grid: *u8, gw: i64, x0: i64, y0: i64, d: i64) -> i64 {
47 let p: *u8 = sp_pat(d)
48 var dist: i64 = 0
49 var y: i64 = 0
50 while y < 5 {
51 var x: i64 = 0
52 while x < 3 {
53 let pix: i64 = grid[(y0 + y) * gw + (x0 + x)]
54 let pb: i64 = p[y * 3 + x] - 0x30
55 if pix != pb { dist = dist + 1 }
56 x = x + 1
57 }
58 y = y + 1
59 }
60 return dist
61}
62
63// template-match the cell at (x0,y0) to the nearest digit. Writes the winning distance (confidence) to
64// out_dist[0]; returns the best digit 0..9.
65func sp_match(grid: *u8, gw: i64, x0: i64, y0: i64, out_dist: *i64) -> i64 {
66 var best: i64 = 0
67 var bestd: i64 = K_MAGIC_9999
68 var d: i64 = 0
69 while d < 10 {
70 let dd: i64 = sp_celldist(grid, gw, x0, y0, d)
71 if dd < bestd { bestd = dd; best = d }
72 d = d + 1
73 }
74 out_dist[0] = bestd
75 return best
76}
77
78// recognize a fixed-pitch row of `ndigits` cells (each `cellw` wide: 3 glyph + gap) starting at x0.
79// writes the recognized digit string (NUL-terminated) to out, and the worst-cell distance to out_maxdist[0].
80func sp_recognize_row(grid: *u8, gw: i64, x0: i64, cellw: i64, ndigits: i64, out: *u8, out_maxdist: *i64) -> i64 {
81 var maxd: i64 = 0
82 let dist: *i64 = sys_mmap(16) as *i64
83 var i: i64 = 0
84 while i < ndigits {
85 let dgt: i64 = sp_match(grid, gw, x0 + i * cellw, 0, dist)
86 out[i] = (0x30 + dgt) as u8
87 if dist[0] > maxd { maxd = dist[0] }
88 i = i + 1
89 }
90 out[ndigits] = 0 as u8
91 out_maxdist[0] = maxd
92 return ndigits
93}
94
95// skip non-digits then read a base-10 integer at text[pos[0]..); advances pos[0].
96func sp_readint(text: *u8, n: i64, pos: *i64) -> i64 {
97 var i: i64 = pos[0]
98 var go: i64 = 1
99 while go == 1 {
100 if i >= n { go = 0 }
101 else { let c: i64 = text[i]; if c >= 0x30 { if c <= 0x39 { go = 0 } else { i = i + 1 } } else { i = i + 1 } }
102 }
103 var v: i64 = 0
104 var go2: i64 = 1
105 while go2 == 1 {
106 if i >= n { go2 = 0 }
107 else { let c: i64 = text[i]; if c >= 0x30 { if c <= 0x39 { v = v * 10 + (c - 0x30); i = i + 1 } else { go2 = 0 } } else { go2 = 0 } }
108 }
109 pos[0] = i
110 return v
111}
112
113// DECODE a NetPBM P1 (ASCII bitmap) image into the pixel grid. dims[0]=width, dims[1]=height.
114// Returns 0 ok, -1 on bad magic / bad dims / over cap.
115func sp_pbm_decode(text: *u8, n: i64, grid: *u8, dims: *i64, cap: i64) -> i64 {
116 if n < 2 { return 0 - 1 }
117 if text[0] != (0x50 as u8) { return 0 - 1 } // 'P'
118 if text[1] != (0x31 as u8) { return 0 - 1 } // '1'
119 let pos: *i64 = sys_mmap(16) as *i64
120 pos[0] = 2
121 let w: i64 = sp_readint(text, n, pos)
122 let h: i64 = sp_readint(text, n, pos)
123 if w <= 0 { return 0 - 1 }
124 if h <= 0 { return 0 - 1 }
125 if w * h > cap { return 0 - 1 }
126 var i: i64 = pos[0]
127 var k: i64 = 0
128 while k < w * h {
129 if i >= n { k = w * h }
130 else {
131 let c: i64 = text[i]
132 if c == 0x30 { grid[k] = 0 as u8; k = k + 1; i = i + 1 }
133 else { if c == 0x31 { grid[k] = 1 as u8; k = k + 1; i = i + 1 } else { i = i + 1 } }
134 }
135 }
136 dims[0] = w
137 dims[1] = h
138 return 0
139}