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}