code wiki / _hdl_build / nx_reader_panels.nx

nx_reader_panels.nx source

↩ module page · 341 lines · 14986 B

1// nx_reader_panels.nx -- SOVEREIGN comic/manga PANEL DETECTOR (reader census SC04 guided-view auto panel 2// detection + SC17 auto-zoom-to-panel; ComiXology-class "intelligent zoom to what's happening"). Given one page 3// image (JPEG or PNG), finds the panels: grayscale -> integer OTSU binarize (ink vs paper) -> recursive X-Y 4// GUTTER-CUT (find the widest interior run of near-inkless columns/rows, split there, iterate with an explicit 5// stack -- no recursion) -> trim each leaf to its ink bounding box -> READING ORDER (band by vertical overlap, 6// then right-to-left for manga rtl=1 / left-to-right rtl=0). Emits panels.json next to the tile pyramid's 7// dz.json, so the viewer zooms panel-to-panel and fetches only that rect's tiles. Panel rects are page-pixel 8// coordinates = resolution-independent ratios for the viewer. Pure integer classical CV (NO ML deps), composes 9// the proven universal decoder nx_img_bytes_to_rgb. LIB (no main) -- nx_reader_panels_gate drives rp_detect and 10// rp_emit directly. license_tier: ORIGINAL 11import "nx_syscalls.nx" 12import "nx_img_bytes_to_rgb.nx" // nx_img_bytes_to_rgb (JPEG+PNG -> RGB) 13const RP_MAGIC_1000000: i64 = 1000000 14const RP_MAGIC_8192: i64 = 8192 15const RP_MAGIC_1024: i64 = 1024 16 17const RP_MAXP: i64 = 64 // max panels per page 18const RP_GUT_PERMIL: i64 = 10 // a gutter row/col has <= 1.0% ink (tolerates JPEG ringing) 19const RP_MIN_DIM: i64 = 24 // min panel width/height (px) 20const RP_MIN_INK: i64 = 100 // min ink pixels for a leaf to count as a panel 21 22func rp_ap_str(dst: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { dst[o + i] = s[i]; i = i + 1 } return o + i } 23func rp_ap_num(dst: *u8, o: i64, v: i64) -> i64 { 24 let t: *u8 = sys_mmap(24); var m: i64 = v; var k: i64 = 0 25 if m == 0 { t[0] = 48 as u8; k = 1 } 26 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 27 var w: i64 = o; var i: i64 = 0 28 while i < k { dst[w] = t[k - 1 - i]; w = w + 1; i = i + 1 } 29 return w 30} 31 32// integer Otsu threshold over a 256-bin histogram (between-class variance argmax). Returns the threshold t: 33// gray <= t is INK. Degenerate (single-mode) histograms return 0 -> no ink -> honest 0 panels. 34func rp_otsu(hist: *i64, total: i64) -> i64 { 35 var sum_all: i64 = 0 36 var i: i64 = 0 37 while i < 256 { sum_all = sum_all + i * hist[i]; i = i + 1 } 38 var w_b: i64 = 0 39 var sum_b: i64 = 0 40 var best_var: i64 = 0 41 var best_t: i64 = 0 42 var t: i64 = 0 43 while t < 256 { 44 w_b = w_b + hist[t] 45 if w_b > 0 { 46 let w_f: i64 = total - w_b 47 if w_f > 0 { 48 sum_b = sum_b + t * hist[t] 49 let m_b: i64 = sum_b / w_b 50 let m_f: i64 = (sum_all - sum_b) / w_f 51 var d: i64 = m_b - m_f 52 if d < 0 { d = 0 - d } 53 let v: i64 = w_b * w_f * d * d / RP_MAGIC_1000000 // /1e6 keeps v well inside i64 for pages up to ~30MP 54 if v > best_var { best_var = v; best_t = t } 55 } 56 } 57 t = t + 1 58 } 59 return best_t 60} 61 62// fill ink[W*H] (1 = ink) from rgb via integer luma + Otsu. Returns total ink pixels. 63func rp_ink_map(rgb: *u8, W: i64, H: i64, ink: *u8) -> i64 { 64 let hist: *i64 = sys_mmap(256 * 8) as *i64 65 var i: i64 = 0 66 while i < 256 { hist[i] = 0; i = i + 1 } 67 let npx: i64 = W * H 68 let gray: *u8 = sys_mmap(npx + 16) 69 i = 0 70 while i < npx { 71 let o: i64 = i * 3 72 let g: i64 = ((rgb[o] as i64) * 299 + (rgb[o + 1] as i64) * 587 + (rgb[o + 2] as i64) * 114) / 1000 73 gray[i] = g as u8 74 hist[g] = hist[g] + 1 75 i = i + 1 76 } 77 let thr: i64 = rp_otsu(hist, npx) 78 var tot: i64 = 0 79 i = 0 80 while i < npx { 81 if (gray[i] as i64) <= thr { ink[i] = 1 as u8; tot = tot + 1 } else { ink[i] = 0 as u8 } 82 i = i + 1 83 } 84 return tot 85} 86 87// count ink in column x of rect rows [y0,y1) 88func rp_colink(ink: *u8, W: i64, x: i64, y0: i64, y1: i64) -> i64 { 89 var c: i64 = 0 90 var y: i64 = y0 91 while y < y1 { if ink[y * W + x] == (1 as u8) { c = c + 1 } y = y + 1 } 92 return c 93} 94// count ink in row y of rect cols [x0,x1) 95func rp_rowink(ink: *u8, W: i64, y: i64, x0: i64, x1: i64) -> i64 { 96 var c: i64 = 0 97 var x: i64 = x0 98 while x < x1 { if ink[y * W + x] == (1 as u8) { c = c + 1 } x = x + 1 } 99 return c 100} 101 102// trim rect [x0,y0,x1,y1) to its ink bounding box; writes trimmed rect into out4. 1 = ink present, 0 = empty. 103func rp_trim(ink: *u8, W: i64, x0: i64, y0: i64, x1: i64, y1: i64, out4: *i64) -> i64 { 104 var nx0: i64 = x0 105 var go: i64 = 1 106 while go == 1 { if nx0 >= x1 { go = 0 } else { if rp_colink(ink, W, nx0, y0, y1) > 0 { go = 0 } else { nx0 = nx0 + 1 } } } 107 if nx0 >= x1 { return 0 } 108 var nx1: i64 = x1 109 go = 1 110 while go == 1 { if nx1 <= nx0 { go = 0 } else { if rp_colink(ink, W, nx1 - 1, y0, y1) > 0 { go = 0 } else { nx1 = nx1 - 1 } } } 111 var ny0: i64 = y0 112 go = 1 113 while go == 1 { if ny0 >= y1 { go = 0 } else { if rp_rowink(ink, W, ny0, nx0, nx1) > 0 { go = 0 } else { ny0 = ny0 + 1 } } } 114 var ny1: i64 = y1 115 go = 1 116 while go == 1 { if ny1 <= ny0 { go = 0 } else { if rp_rowink(ink, W, ny1 - 1, nx0, nx1) > 0 { go = 0 } else { ny1 = ny1 - 1 } } } 117 out4[0] = nx0; out4[1] = ny0; out4[2] = nx1; out4[3] = ny1 118 return 1 119} 120 121// widest run of gutter COLUMNS strictly inside (x0,x1) of a trimmed rect; writes [start,end) into out2. 122// Returns run width (0 = none). A gutter col has ink_permil <= RP_GUT_PERMIL of the rect height. 123func rp_gutter_cols(ink: *u8, W: i64, x0: i64, y0: i64, x1: i64, y1: i64, out2: *i64) -> i64 { 124 let h: i64 = y1 - y0 125 var bestw: i64 = 0 126 var run0: i64 = 0 - 1 127 var x: i64 = x0 128 while x < x1 { 129 let c: i64 = rp_colink(ink, W, x, y0, y1) 130 var isgut: i64 = 0 131 if c * 1000 <= h * RP_GUT_PERMIL { isgut = 1 } 132 if isgut == 1 { 133 if run0 < 0 { run0 = x } 134 } else { 135 if run0 >= 0 { 136 if run0 > x0 { // interior only: ink on the left of the run 137 let w2: i64 = x - run0 138 if w2 > bestw { bestw = w2; out2[0] = run0; out2[1] = x } 139 } 140 run0 = 0 - 1 141 } 142 } 143 x = x + 1 144 } 145 // a run touching x1 is NOT interior (trim already removed margins; ink must exist on both sides) 146 return bestw 147} 148// widest run of gutter ROWS strictly inside (y0,y1); mirrors rp_gutter_cols. 149func rp_gutter_rows(ink: *u8, W: i64, x0: i64, y0: i64, x1: i64, y1: i64, out2: *i64) -> i64 { 150 let w2: i64 = x1 - x0 151 var bestw: i64 = 0 152 var run0: i64 = 0 - 1 153 var y: i64 = y0 154 while y < y1 { 155 let c: i64 = rp_rowink(ink, W, y, x0, x1) 156 var isgut: i64 = 0 157 if c * 1000 <= w2 * RP_GUT_PERMIL { isgut = 1 } 158 if isgut == 1 { 159 if run0 < 0 { run0 = y } 160 } else { 161 if run0 >= 0 { 162 if run0 > y0 { 163 let h2: i64 = y - run0 164 if h2 > bestw { bestw = h2; out2[0] = run0; out2[1] = y } 165 } 166 run0 = 0 - 1 167 } 168 } 169 y = y + 1 170 } 171 return bestw 172} 173 174// count ink inside a rect (for the leaf min-ink filter) 175func rp_rect_ink(ink: *u8, W: i64, x0: i64, y0: i64, x1: i64, y1: i64) -> i64 { 176 var c: i64 = 0 177 var y: i64 = y0 178 while y < y1 { c = c + rp_rowink(ink, W, y, x0, x1); y = y + 1 } 179 return c 180} 181 182// order panels for reading: band panels by vertical position (a new band starts when a panel's y-center is 183// below every prior band member's bottom), then within a band sort right-to-left (rtl=1) or left-to-right. 184// pan = flat quads [x,y,w,h] * n, reordered in place. Bubble sorts (n <= RP_MAXP = tiny). 185func rp_order(pan: *i64, n: i64, rtl: i64) -> i64 { 186 // 1) sort by y-center 187 var i: i64 = 0 188 while i < n { 189 var j: i64 = 0 190 while j < n - 1 - i { 191 let yc_a: i64 = pan[j * 4 + 1] * 2 + pan[j * 4 + 3] 192 let yc_b: i64 = pan[(j + 1) * 4 + 1] * 2 + pan[(j + 1) * 4 + 3] 193 if yc_a > yc_b { 194 var k: i64 = 0 195 while k < 4 { let t: i64 = pan[j * 4 + k]; pan[j * 4 + k] = pan[(j + 1) * 4 + k]; pan[(j + 1) * 4 + k] = t; k = k + 1 } 196 } 197 j = j + 1 198 } 199 i = i + 1 200 } 201 // 2) assign bands 202 let band: *i64 = sys_mmap(8 * RP_MAXP) as *i64 203 var bid: i64 = 0 204 var bbot: i64 = 0 - 1 205 i = 0 206 while i < n { 207 let yc: i64 = pan[i * 4 + 1] * 2 + pan[i * 4 + 3] // 2*y + h = 2*ycenter 208 let bot: i64 = (pan[i * 4 + 1] + pan[i * 4 + 3]) * 2 // 2*bottom 209 if bbot >= 0 { if yc > bbot { bid = bid + 1; bbot = 0 - 1 } } 210 if bbot < 0 { bbot = bot } else { if bot > bbot { bbot = bot } } 211 band[i] = bid 212 i = i + 1 213 } 214 // 3) within a band, sort by x (desc when rtl) 215 i = 0 216 while i < n { 217 var j2: i64 = 0 218 while j2 < n - 1 - i { 219 var swap: i64 = 0 220 if band[j2] == band[j2 + 1] { 221 let xa: i64 = pan[j2 * 4] 222 let xb: i64 = pan[(j2 + 1) * 4] 223 if rtl == 1 { if xa < xb { swap = 1 } } else { if xa > xb { swap = 1 } } 224 } 225 if swap == 1 { 226 var k2: i64 = 0 227 while k2 < 4 { let t2: i64 = pan[j2 * 4 + k2]; pan[j2 * 4 + k2] = pan[(j2 + 1) * 4 + k2]; pan[(j2 + 1) * 4 + k2] = t2; k2 = k2 + 1 } 228 let tb: i64 = band[j2]; band[j2] = band[j2 + 1]; band[j2 + 1] = tb 229 } 230 j2 = j2 + 1 231 } 232 i = i + 1 233 } 234 return 0 235} 236 237// detect panels on an in-memory RGB page. pan = caller buffer for RP_MAXP flat [x,y,w,h] quads. 238// Returns panel count (0 = blank/undetectable -> viewer falls back to whole-page). 239func rp_detect(rgb: *u8, W: i64, H: i64, rtl: i64, pan: *i64) -> i64 { 240 if W < RP_MIN_DIM { return 0 } 241 if H < RP_MIN_DIM { return 0 } 242 let ink: *u8 = sys_mmap(W * H + 16) 243 let tot: i64 = rp_ink_map(rgb, W, H, ink) 244 if tot < RP_MIN_INK { return 0 } 245 246 // explicit work stack of rects (no recursion) 247 let st: *i64 = sys_mmap(8 * 4 * 256) as *i64 248 var sp: i64 = 0 249 let t4: *i64 = sys_mmap(64) as *i64 250 if rp_trim(ink, W, 0, 0, W, H, t4) == 0 { return 0 } 251 st[0] = t4[0]; st[1] = t4[1]; st[2] = t4[2]; st[3] = t4[3]; sp = 1 252 253 let g2: *i64 = sys_mmap(64) as *i64 254 var np: i64 = 0 255 var guard: i64 = 0 256 while sp > 0 { 257 guard = guard + 1 258 if guard > 512 { sp = 0 } else { 259 sp = sp - 1 260 let x0: i64 = st[sp * 4]; let y0: i64 = st[sp * 4 + 1]; let x1: i64 = st[sp * 4 + 2]; let y1: i64 = st[sp * 4 + 3] 261 let rw: i64 = x1 - x0 262 let rh: i64 = y1 - y0 263 var minrun_c: i64 = rw / 120; if minrun_c < 6 { minrun_c = 6 } 264 var minrun_r: i64 = rh / 120; if minrun_r < 6 { minrun_r = 6 } 265 let cw: i64 = rp_gutter_cols(ink, W, x0, y0, x1, y1, g2) 266 var cs: i64 = 0; var ce: i64 = 0 267 if cw >= minrun_c { cs = g2[0]; ce = g2[1] } 268 let rw2: i64 = rp_gutter_rows(ink, W, x0, y0, x1, y1, g2) 269 var rs: i64 = 0; var re: i64 = 0 270 if rw2 >= minrun_r { rs = g2[0]; re = g2[1] } 271 var split: i64 = 0 // 0 none / 1 cols / 2 rows 272 if cw >= minrun_c { split = 1 } 273 if rw2 >= minrun_r { if split == 0 { split = 2 } else { if rw2 > cw { split = 2 } } } 274 if split == 1 { 275 if sp + 2 <= 256 { 276 // left half [x0, cs) and right half [ce, x1) -- trim each before pushing 277 if rp_trim(ink, W, x0, y0, cs, y1, t4) == 1 { st[sp * 4] = t4[0]; st[sp * 4 + 1] = t4[1]; st[sp * 4 + 2] = t4[2]; st[sp * 4 + 3] = t4[3]; sp = sp + 1 } 278 if rp_trim(ink, W, ce, y0, x1, y1, t4) == 1 { st[sp * 4] = t4[0]; st[sp * 4 + 1] = t4[1]; st[sp * 4 + 2] = t4[2]; st[sp * 4 + 3] = t4[3]; sp = sp + 1 } 279 } 280 } else { if split == 2 { 281 if sp + 2 <= 256 { 282 if rp_trim(ink, W, x0, y0, x1, rs, t4) == 1 { st[sp * 4] = t4[0]; st[sp * 4 + 1] = t4[1]; st[sp * 4 + 2] = t4[2]; st[sp * 4 + 3] = t4[3]; sp = sp + 1 } 283 if rp_trim(ink, W, x0, re, x1, y1, t4) == 1 { st[sp * 4] = t4[0]; st[sp * 4 + 1] = t4[1]; st[sp * 4 + 2] = t4[2]; st[sp * 4 + 3] = t4[3]; sp = sp + 1 } 284 } 285 } else { 286 // leaf -> panel (already trimmed) 287 var keep: i64 = 1 288 if rw < RP_MIN_DIM { keep = 0 } 289 if rh < RP_MIN_DIM { keep = 0 } 290 if keep == 1 { if rp_rect_ink(ink, W, x0, y0, x1, y1) < RP_MIN_INK { keep = 0 } } 291 if keep == 1 { if np < RP_MAXP { 292 pan[np * 4] = x0; pan[np * 4 + 1] = y0; pan[np * 4 + 2] = rw; pan[np * 4 + 3] = rh 293 np = np + 1 294 } } 295 } } 296 } 297 } 298 rp_order(pan, np, rtl) 299 return np 300} 301 302// end-to-end: decode page file -> detect -> write <outdir>/panels.json 303// {"w":W,"h":H,"rtl":R,"n":N,"panels":[[x,y,w,h],...]}. Returns panel count, or negative error 304// (-1 read, -2 decode). 0 panels still writes an honest empty manifest (viewer falls back to whole page). 305func rp_emit(src: *u8, outdir: *u8, rtl: i64) -> i64 { 306 let szbox: *i64 = sys_mmap(16) as *i64 307 let data: *u8 = sys_read_file(src, szbox) 308 if (data as i64) == 0 { return 0 - 1 } 309 let wh: *i64 = sys_mmap(16) as *i64 310 let rgb: *u8 = nx_img_bytes_to_rgb(data, szbox[0], wh) 311 if (rgb as i64) == 0 { return 0 - 2 } 312 let W: i64 = wh[0]; let H: i64 = wh[1] 313 let pan: *i64 = sys_mmap(8 * 4 * RP_MAXP) as *i64 314 let np: i64 = rp_detect(rgb, W, H, rtl, pan) 315 316 __syscall(258, 0 - 100, outdir as i64, 0x1ed, 0, 0, 0) // mkdir (idempotent) 317 let jb: *u8 = sys_mmap(RP_MAGIC_8192) 318 var o: i64 = rp_ap_str(jb, 0, "{\"w\":" as *u8); o = rp_ap_num(jb, o, W) 319 o = rp_ap_str(jb, o, ",\"h\":" as *u8); o = rp_ap_num(jb, o, H) 320 o = rp_ap_str(jb, o, ",\"rtl\":" as *u8); o = rp_ap_num(jb, o, rtl) 321 o = rp_ap_str(jb, o, ",\"n\":" as *u8); o = rp_ap_num(jb, o, np) 322 o = rp_ap_str(jb, o, ",\"panels\":[" as *u8) 323 var i: i64 = 0 324 while i < np { 325 if i > 0 { jb[o] = 44 as u8; o = o + 1 } 326 jb[o] = 91 as u8; o = o + 1 327 o = rp_ap_num(jb, o, pan[i * 4]); jb[o] = 44 as u8; o = o + 1 328 o = rp_ap_num(jb, o, pan[i * 4 + 1]); jb[o] = 44 as u8; o = o + 1 329 o = rp_ap_num(jb, o, pan[i * 4 + 2]); jb[o] = 44 as u8; o = o + 1 330 o = rp_ap_num(jb, o, pan[i * 4 + 3]) 331 jb[o] = 93 as u8; o = o + 1 332 i = i + 1 333 } 334 o = rp_ap_str(jb, o, "]}" as *u8) 335 let pp: *u8 = sys_mmap(RP_MAGIC_1024) 336 var po: i64 = rp_ap_str(pp, 0, outdir); po = rp_ap_str(pp, po, "/panels.json" as *u8); pp[po] = 0 as u8 337 let fd: i64 = sys_openat_wr(pp, 0x1a4) 338 if fd < 0 { return 0 - 1 } 339 sys_write(fd, jb, o); sys_close(fd) 340 return np 341}