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}