code wiki / (root) / nx_doc_scan.nx

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}