code wiki / (root) / nx_manga_ocr.nx

nx_manga_ocr.nx source

↩ module page · 93 lines · 4350 B

1// nx_manga_ocr.nx -- MG22: FIND THE TEXT REGIONS ON A PAGE (lib; lib+gate split, no main). 2// The inverse of the lettering compositor: mb_bubble_draw DRAWS text into a box, this FINDS the box 3// again. That makes the referee free -- our own emitter can produce any fixture this detector must 4// recover -- which is why this rung needs no corpus and no model, unlike MG21 CJK recognition. 5// 6// COMPOSES THE INCUMBENT AND RE-IMPLEMENTS NOTHING: nx_reader_panels already ships the ink primitives 7// this needs -- rp_ink_map (integer Otsu binarize, ink vs paper), rp_rowink and rp_colink (ink counts 8// over a row or column segment). Panel detection and text detection are the SAME projection machinery 9// at different scales: a panel is separated by a wide gutter, a text line by a narrow leading. Writing 10// a second ink scanner beside that one would be the duplicate-ruler defect. 11// 12// METHOD, and its honest scope: horizontal projection groups inked rows into BANDS separated by more 13// than MO_LINE_GAP rows of paper, then a vertical projection inside each band gives its extent. That 14// finds INK CLUSTERS -- text lines and blocks. It does NOT yet distinguish a speech bubble from a 15// caption or from dense hatching, and it does not read the bubble OUTLINE; that is the next rung and 16// this header says so rather than letting a reader assume otherwise. 17// 18// POLICY-AS-CONSTS, DEBT DECLARED: the four thresholds below are named rather than buried, but named 19// is not data -- they belong in a conf row beside knowledge/manga/layouts.conf, exactly as 20// nx_manga_bubble's lettering style does. Filed as MG22-conf rather than left silent. 21 22import "nx_syscalls.nx" 23import "nx_reader_panels.nx" 24 25const MO_MAXR: i64 = 64 // max regions returned; a FLOOR is announced by the caller, never silent 26const MO_MIN_INK: i64 = 2 // a row/col with fewer inked pixels than this counts as paper 27const MO_LINE_GAP: i64 = 3 // rows of paper that still belong to ONE text block (leading, not a break) 28const MO_MIN_W: i64 = 3 // a region narrower than this is speckle, not text 29const MO_MIN_H: i64 = 3 // a region shorter than this is speckle, not text 30const MO_FIELDS: i64 = 4 // x0 y0 x1 y1 per region 31 32// mo_region_count -- how many regions fit in a caller buffer of `cap` i64s. 33func mo_region_count(cap: i64) -> i64 { return cap / MO_FIELDS } 34 35// mo_text_regions -- ink map in, region rects out. Returns the COUNT; out holds MO_FIELDS per region. 36// A return of MO_MAXR is a FLOOR, not a total: the caller must treat it as truncated and say so. 37func mo_text_regions(ink: *u8, W: i64, x0: i64, y0: i64, x1: i64, y1: i64, out: *i64) -> i64 { 38 var n: i64 = 0 39 var y: i64 = y0 40 while y < y1 { 41 // advance over paper rows 42 var scanning: i64 = 1 43 while scanning == 1 { 44 if y >= y1 { scanning = 0 } 45 else { 46 if rp_rowink(ink, W, y, x0, x1) >= MO_MIN_INK { scanning = 0 } 47 else { y = y + 1 } 48 } 49 } 50 if y >= y1 { return n } 51 let top: i64 = y 52 var bot: i64 = y 53 var gap: i64 = 0 54 var inband: i64 = 1 55 while inband == 1 { 56 if y >= y1 { inband = 0 } 57 else { 58 if rp_rowink(ink, W, y, x0, x1) >= MO_MIN_INK { bot = y; gap = 0; y = y + 1 } 59 else { 60 gap = gap + 1 61 if gap > MO_LINE_GAP { inband = 0 } else { y = y + 1 } 62 } 63 } 64 } 65 // vertical extent of this band 66 var lx: i64 = x1 67 var rx: i64 = x0 - 1 68 var x: i64 = x0 69 while x < x1 { 70 if rp_colink(ink, W, x, top, bot + 1) >= MO_MIN_INK { 71 if x < lx { lx = x } 72 if x > rx { rx = x } 73 } 74 x = x + 1 75 } 76 if rx >= lx { 77 let w: i64 = rx - lx + 1 78 let h: i64 = bot - top + 1 79 if w >= MO_MIN_W { 80 if h >= MO_MIN_H { 81 if n < MO_MAXR { 82 out[n * MO_FIELDS + 0] = lx 83 out[n * MO_FIELDS + 1] = top 84 out[n * MO_FIELDS + 2] = rx 85 out[n * MO_FIELDS + 3] = bot 86 n = n + 1 87 } 88 } 89 } 90 } 91 } 92 return n 93}