🎨 image-editorHomeUser GuidePro GuideInstallArchitectureAnalysisPlanChangelog

image-editor β€” Architecture & Developer Guide

Overview

image-editor separates a Qt-free editing core from the Qt view, mediated by a controller β€” the same discipline as image_selector, so the core can also back a headless/MCP process.

The design rule: every image operation takes a NumPy array and returns one. The UI only assembles state and displays results.


Module map

image-editor/
β”œβ”€β”€ main.py                 Entry point β€” boots Qt, wires controller + window
β”œβ”€β”€ config.py               JSON config (~/.config/image-editor/)
β”œβ”€β”€ img_io.py               Unicode-safe imread/imwrite (+ alpha-aware read, quality params)
β”œβ”€β”€ app_controller.py       Mediates Document ↔ widgets; global/local edit state
β”œβ”€β”€ core/                   Qt-free engine
β”‚   β”œβ”€β”€ document.py         Document = base image + layer stack; render(scale)
β”‚   β”œβ”€β”€ layers.py           Layer types (adjust / adjust_set / heal / paste / warp) + compositing
β”‚   β”œβ”€β”€ mask.py             Mask recipes: brush, shapes, gradients, luminance, RasterSource
β”‚   β”œβ”€β”€ ops.py              Per-pixel adjustments + op registry
β”‚   β”œβ”€β”€ curves.py           Monotone-spline (PCHIP) β†’ 256-LUT
β”‚   β”œβ”€β”€ film_luts.py        Camera profiles + Fujifilm-inspired film simulations
β”‚   β”œβ”€β”€ blend.py            Composite + blend modes, seamless paste, colour match
β”‚   β”œβ”€β”€ heal.py             Inpaint dispatch (classical β†’ LaMa)
β”‚   β”œβ”€β”€ warp.py             Liquify / reshape displacement field
β”‚   β”œβ”€β”€ project.py          .iedit save/load (JSON + embedded arrays)
β”‚   └── backends/           Optional ML, lazily imported
β”‚       β”œβ”€β”€ segment.py      SAM box/point segmentation
β”‚       └── inpaint_lama.py LaMa erase
└── widgets/
    β”œβ”€β”€ main_window.py      Window, toolbar, tabbed rail, background jobs
    β”œβ”€β”€ canvas.py           Zoom/pan preview + brush / crop / warp tools + selection overlay
    └── curve_editor.py     Draggable tone-curve widget

The document model

@dataclass
class Document:
    base: np.ndarray                 # immutable original, BGR uint8, full res
    layers: list[Layer]              # ordered, bottom β†’ top
    crop_rect: tuple | None          # normalised, post-rotation
    rotation: int                    # 0 / 90 / 180 / 270
    # + undo/redo history over (layers, crop, rotation)

A Layer produces an effect image, which the document composites back through the layer's mask and blend mode β€” so a global adjustment and a local one share one code path (global = no mask). Expensive layers (heal, paste, warp) cache their result per resolution.

Masks are resolution-independent recipes

Mask holds a list of sources (brush strokes, shapes, gradients, luminance ranges, or a RasterSource from SAM) in normalised coordinates, plus feather and an optional edge-aware (guided-filter) refine. resolve(h, w) rasterizes to a float [0,1] map at any resolution β€” so a mask drives both the proxy preview and the full-res export.


The render pipeline

AppController.render(full=False) is the single source of truth, and it renders pending previews in the same position they'll occupy once committed, so preview == final:

base = Document.render(scale)          # committed layers, geometry
  β†’ pending local-selection edit       # (uncommitted) masked adjust_set
  β†’ pending paste                       # (uncommitted) seamless clone
  β†’ pending reshape/warp                # (uncommitted) remap
  β†’ global grade                        # adjust sliders + curves + film look

scale < 1 renders a fast proxy for live preview; Save always calls render(full=True) and writes atomically (tempfile β†’ move) with the original timestamp preserved.


Optional ML backends

core/backends/ is lazily imported and always guarded β€” if a package or model is missing, the feature degrades (Erase β†’ classical inpaint; Remove person β†’ unavailable). Heavy calls run on a QThread (_RenderWorker) so the UI never freezes. See installation.md for the CPU-only install recipes and the licence note.


Testing

tests/test_core.py exercises the Qt-free core with pytest: curves, ops, masks, compositing, geometry, layers, project round-trips, and (when the backends are installed) a full SAM→LaMa person removal.

.venv/bin/python -m pytest tests -q

Further reading