Skip to content

ImageBuffer

aglaia/ImageBuffer.py:ImageBuffer — the canonical envelope passed between every part of the system.

Fields

FieldTypePurpose
buffernp.ndarrayPixel data. RGB (H,W,3), Gray (H,W), or BW (H,W with values in {0,255}).
typeImageTypeCOLOR, GRAY, or BW. Determines write format (jpg/png) and helper conversion.
dpifloatImage DPI. Used by DPIfixer, Binarizer template eval, page margin calc, dewarp.
pathstr?Source file path (raw image) — used for routing GUI events.
parentImageBuffer?Parent buffer (set on child crops by PageDetector).
parent_stemstr?Parent filestem cached as string so child copies don’t drag the whole parent through pickling.
filestemstr?Output filename stem (no extension). E.g. mybook_001, mybook_001_A.
out_dirstr?Disk-write hint for ImageBuffer.write (see below). The live chain persists to the project DB and ignores it.
scan_id, parent_node_id, pipeline_version_id, branch_path, branch_label, depthint?/strDB tree context — carried by the chain so Persister can link each step’s node to its scan, parent node and branch.
childrenlist[ImageBuffer]Set by branching processors (PageDetector).
metadictPer-buffer metadata. See below.

meta keys — the schema

Every key has a declared kind (aglaia/meta_schema.py), and the kind decides what a warp does with it. Geometric kinds are moved by the transform machinery; records stay on the node that produced them; everything else is copied. An undeclared key is dropped at the first warp, with a log line — an undeclared coordinate list passing through untransformed is exactly the DPIfixer bug (155ab37), so “pass it along, it’s probably fine” is the failure mode, not the safe default.

Plugins declare theirs at import: declare_meta("stamps_found", MetaKind.SCALAR) from aglaia.plugin_api. Keys starting with _ are a processor’s private scratch and are OPAQUE by rule.

tests/test_meta_schema.py scans the source for every meta["…"] literal and fails on an undeclared one, and checks this table lists every declared key — neither the code nor this page can drift from the schema.

KeyKindSet by
roipolygon — transformed by every warpPageDetector (child outline); every COORDINATE step moves it
erasepolygons — transformedany erase producer (StampRemover); moved by every COORDINATE step
parent_crop_xywhrect [x,y,w,h] — transformed through an affine, dropped through a dewarpPageDetector
column_quadgeometric record in the producer’s own input frame — kept for its debug view, never carried downstreamTrapezoidalCorrection
line_boxesgeometric record in the producer’s own input frame — kept for its debug view, never carried downstreamTrapezoidalCorrection
Hgeometric record in the producer’s own input frame — kept for its debug view, never carried downstreamTrapezoidalCorrection
page_numsgeometric record in the producer’s own input frame — kept for its debug view, never carried downstream(debug overlay)
page_sidelabel — copiedPageDetector
erase_sourceslabel — copiederase.py
manuallabel — copiedmanual.py (which fields were hand-edited)
manual_droppedlabel — copiedmanual.py
replay_kindlabel — copiedevery COORDINATE / PIXEL_VALUE step
fallback_reasonlabel — copiedPageDewarper
column_edge_sourcelabel — copiedTrapezoidalCorrection
line_sourcelabel — copiedTrapezoidalCorrection
mode_usedlabel — copiedTrapezoidalCorrection
skew_anglescalar — copiedSkewFinder
skewscalar — copiedSkewFinder (the GUI’s name, written directly)
successscalar — copiedPageDewarper
oobscalar — copiedPageDewarper (out-of-bounds fraction)
char_h_fracscalar — copiedTrapezoidalCorrection, PageDewarper
recovered_aspect_w_hscalar — copiedTrapezoidalCorrection
dewarp_successscalar — copiedPageDewarper
trapezoid_successscalar — copiedTrapezoidalCorrection
oob_forcedscalar — copiedPageDewarper
oob_pctscalar — copiedPageDewarper / manual quad path
statusscalar — copiedchain / PageDewarper
elapsed_msscalar — copiedchain
disabledscalar — copiedchain
gpuscalar — copiedchain
n_baselinesscalar — copiedTrapezoidalCorrection
n_full_widthscalar — copiedTrapezoidalCorrection
n_vblocksscalar — copiedTrapezoidalCorrection
n_vp_inliersscalar — copiedTrapezoidalCorrection
vp_inlier_fracscalar — copiedTrapezoidalCorrection
recovered_focal_pxscalar — copiedTrapezoidalCorrection
stamps_foundscalar — copiedStampRemover (plugin)
replay_paramsopaque — copied, never interpreted in transitevery replayable step — the replay engine owns its shape
oob_statsopaque — copied, never interpreted in transitPageDewarper
manual_overridesopaque — copied, never interpreted in transitchain (this branch’s hand edits)
manual_overrides_allopaque — copied, never interpreted in transitchain (every branch’s)
frame_whopaque — copied, never interpreted in transitdebug renderers
erase_frame_whopaque — copied, never interpreted in transitdebug viewer (erase edits)
layouts_frame_whopaque — copied, never interpreted in transitdebug viewer (layout edits)

Undeclared/private in the wild: _dewarp_* (PageDewarper solver state, OPAQUE by the _ rule).

When emitting image_event, _emit_event normalizes some keys for the GUI:

  • dewarp_success → success
  • oob_stats → oob
  • skew_angle → skew

Helpers

MethodBehavior
to_rgb()Returns 3-channel RGB ndarray (converts gray/RGBA). Non-mutating.
to_gray()Returns single-channel grayscale. Non-mutating.
to_bw()Returns Otsu-thresholded BW. Non-mutating.
check_binary()True if 2D with ≤2 unique values.
rescale(target_dpi, threshold)Resizes buffer to match target_dpi (skip if diff < threshold). Mutates.
copy()Deep clone of buffer + deepcopy of meta. Does NOT deep-copy parent (keeps reference / parent_stem string).
deskew(...)Estimate + apply skew in place. Prefer the SkewFinder processor.

binarize(), detectLayouts(), dewarp() are duck-typing convenience helpers.

write(collection, options, suffix=None, …)

ImageBuffer keeps a general-purpose disk writer (aglaia/ImageBuffer.py). It is not called by the live IntegratedProcessingChain — that path persists each step to the project .agl SQLite DB via aglaia/storage/persister.py. The logic below documents the envelope’s own write behaviour, used by off-chain helpers.

Decides where to save based on this priority:

  1. options["paths"][collection] if set.
  2. options["paths"]["root"] if set → <root>/<collection>/<stem>.{ext}.
  3. Existing self.out_dir (PDF mode), with sibling-folder heuristics:
    • If out_dir equals the output root, redirect to its parent + collection.
    • If collection == "output", respect out_dir exactly.
    • Otherwise rewrite to <out_dir.parent>/<collection>.
  4. Fallback: paths["output"].parent / collection.

Filename:

  • self.filestem (or paths["filestem"], or "capture") + optional _<suffix>.
  • Extension: .png if type == BW, else .jpg.

Before writing, same-stem conflicts in the target folder are unlinked (jpg ↔ png hygiene).

Output format:

  • .jpg — PIL save, RGB or L mode, quality=95, optimize=true, with dpi=(dpi, dpi) EXIF.
  • .png — BW via PIL '1' mode (1bpp), or OpenCV cv2.imwrite for color.

executor: optional ThreadPoolExecutor for async writes — only relevant to off-chain write callers; the live chain does not use this method.

After a successful write, an image_event tuple is pushed on log_queue if event_type is set:

('image_event',
raw_path, # absolute normalized path of the original raw file (walked via parent chain)
filestem,
event_type, # usually the chain step's instance_name
full_path, # absolute normalized path of just-written file
gui_meta, # meta dict with key normalization
parent_stem)

Lifecycle examples

Capture (single page)

raw_frame (cv2 BGR)
→ cv2.cvtColor BGR→RGB
→ ImageBuffer(buf=rgb, type=COLOR, dpi=capture_dpi, filestem='mybook_001', scan_id=...)
→ input queue.put(...)
→ worker runs full pipeline in place; persist_step → DB node+image per step

PDF (2-page spread)

PDF page → PIL Image (extracted or rendered at median DPI)
→ raw stored as a scan in the project DB
→ ImageBuffer(COLOR, dpi, filestem=stem, scan_id=...)
→ input queue.put(ib)
→ worker pipeline: DPIfixer → SkewFinder → PageDetector
PageDetector emits buf.children = [child_A, child_B]
chain persists the parent node (marks it a branch point),
persists each child node, re-injects each as a DB ref at step 4
→ each child: DPIfixer (300) → Binarizer → SkewFinder → PageDewarper
→ each step persisted as a DB node+image (branch labels A, B)

Pitfalls

  • Always copy() before crossing process boundaries. Copy a buffer before persisting or re-injecting it to avoid mutating the in-flight buffer.
  • BW palette preservation: after a non-binarizing step runs on a BW input, the chain re-binarizes the result with binarize_fixed(127) so jpg compression artifacts don’t accumulate.
  • roi lives in parent coordinates until PageDetector remaps it via cv2.intersectConvexConvex (then it’s in child coordinates).
  • dpi is metadata only — the buffer is not automatically rescaled when you set it. Use DPIfixer (or rescale()).