🎨 image-editorHomeUser GuidePro GuideInstallArchitectureAnalysisPlanChangelog

image-editor β€” Implementation Plan

Companion to analysis.md. Turns the research into a concrete, phased build. Reuses image_selector's conventions: PySide6/Qt UI, Qt-decoupled pure-array ops, atomic Unicode-safe full-resolution save.

Date: 2026-09-11 Β· Status: planning


Guiding constraints (from analysis Β§0)

  1. Full-resolution, non-destructive β€” preview on a proxy, always render the final at native resolution with format/quality control.
  2. Photoshop-capable, phone-app-simple β€” power (masks/layers/inpainting) lives under a direct-on-image UI. Every feature ships an algorithm and a simple interaction, together.
  3. Models are optional β€” phases 1–5 run with zero ML deps; ML degrades gracefully when a model/GPU is absent.

Target architecture

MVC like image_selector, but the "model" grows from a flat EditState into a Document (layer stack). The core is Qt-free so it can back an MCP server later.

image-editor/
β”œβ”€β”€ main.py                   # entry point β€” boots Qt, opens Document, wires window
β”œβ”€β”€ config.py                 # JSON config (~/.config/image-editor/)
β”œβ”€β”€ img_io.py                 # COPIED from image_selector β€” Unicode-safe imread/imwrite
β”œβ”€β”€ core/                     # zero Qt imports β€” pure numpy/opencv
β”‚   β”œβ”€β”€ document.py           # Document, layer stack, render(), history
β”‚   β”œβ”€β”€ layers.py             # Layer types (Adjustment, Heal, Pixel, ...)
β”‚   β”œβ”€β”€ mask.py               # Mask type + sources (shapes, brush, parametric, ML)
β”‚   β”œβ”€β”€ ops.py                # ported edit_ops.py β€” brightness/contrast/curves/...
β”‚   β”œβ”€β”€ film_luts.py          # COPIED from image_selector (film sims as ops)
β”‚   β”œβ”€β”€ curves.py             # monotone-spline β†’ 256-LUT
β”‚   β”œβ”€β”€ blend.py              # alpha composite, blend modes, seamlessClone, colour-match
β”‚   β”œβ”€β”€ heal.py               # inpaint dispatch: telea | patchmatch | lama
β”‚   β”œβ”€β”€ render.py             # proxy vs full-res rendering, export (quality/format)
β”‚   └── backends/             # ML wrappers, lazy-imported, optional
β”‚       β”œβ”€β”€ segment.py        # SAM 2 + rembg  (returns a Mask)
β”‚       └── inpaint_lama.py   # LaMa erase (+ optional SD)
β”œβ”€β”€ widgets/                  # Qt view layer
β”‚   β”œβ”€β”€ main_window.py        # window, toolbar, keyboard, open/save
β”‚   β”œβ”€β”€ canvas.py             # zoom/pan preview (port preview_widget.py) + gestures
β”‚   β”œβ”€β”€ tool_overlay.py       # on-canvas brush/shape/handles (port crop overlay)
β”‚   β”œβ”€β”€ tools_panel.py        # the simple tool rail (Erase, Adjust, Curves, Crop...)
β”‚   └── adjust_panel.py       # slider/curve UI for the active tool
β”œβ”€β”€ app_controller.py         # actions ↔ Document ↔ widgets
β”œβ”€β”€ requirements.txt
└── docs/{analysis.md, plan.md, changelog.md, versioning/...}

Core data model

# core/mask.py
@dataclass
class Mask:
    data: np.ndarray            # float32 HxW in [0,1], image-resolution (or None = all-ones)
    invert: bool = False
    feather_px: float = 0.0     # gaussian fallback
    edge_aware: bool = False    # guided-filter refine against image
    def resolve(self, img) -> np.ndarray: ...   # -> concrete float32 HxW in [0,1]

# core/layers.py
@dataclass
class Layer:
    kind: str                   # "adjust" | "heal" | "pixel"
    params: dict                # op name + values, or heal engine, or rgba+transform
    mask: Mask | None = None
    blend_mode: str = "normal"
    opacity: float = 1.0
    visible: bool = True

# core/document.py
@dataclass
class Document:
    base: np.ndarray            # immutable original BGR (full res)
    layers: list[Layer]
    crop_rect: tuple | None = None
    rotation: int = 0
    def render(self, scale: float = 1.0) -> np.ndarray: ...   # proxy or full-res

render() = start from (rotated, cropped) base, apply each visible layer top-to-bottom via blend.composite(base, effect, mask, mode, opacity). The same method renders the proxy (scale<1, for live preview) and the full-res export (scale=1) β€” guaranteeing what you see is what you save.


Phase 0 β€” Project scaffold (0.5 day)

Phase 1 β€” Document, layers, compositor, masks (3–4 days) β€” the spine

Phase 2 β€” Local adjustments (2 days)

Phase 3 β€” Curves (1–2 days)

Phase 4 β€” Classical heal / erase (1–2 days)

Phase 5 β€” Seamless paste + colour match (2 days)

Phases 0–5 ship a genuinely useful editor with zero ML dependencies. Everything below is additive and optional.

Phase 6 β€” AI segmentation (2–3 days)

Phase 7 β€” LaMa erase (2–3 days) β€” the headline feature

Phase 8 β€” Optional extras (as desired)


Phase 9 β€” Pro-grade capabilities (the "professional look")

From the professional-editing research in analysis.md Β§11. Ordered by leverage on a professional result Γ· effort. Items 9.1–9.6 are pure OpenCV/NumPy (no new deps) and slot into the existing layer/mask/curve engine β€” do these first. 9.7–9.10 follow the optional-backend pattern proven with LaMa/SAM.

No new dependencies (highest leverage first)

Optional backends (bigger, higher image-quality tier)

Suggested order: 9.2 β†’ 9.4 β†’ 9.3 β†’ 9.1 β†’ 9.5 β†’ 9.8 β†’ 9.6 β†’ 9.7, then the backend tier 9.9 β†’ 9.10 β†’ 9.12 β†’ 9.11. Until RAW/denoise/upscale land, pair image-editor with a free RAW processor (Darktable / RawTherapee) for those steps.


Cross-cutting concerns

Rough sequencing

Milestone Phases Outcome
M1 β€” "better than the phone" 0–3 full-res open/edit/save, masks, local adjust, curves β€” no ML
M2 β€” classical retouch 4–5 heal small stuff, seamless paste, colour match
M3 β€” "erase anyone" 6–7 SAM tap-select + LaMa erase, the headline feature
M4 β€” creative/optional 8 SD replace/outpaint, AI looks, RAW/HEIC, batch
M5 β€” pro-grade look 9 colour grading/HSL, WB picker, clarity/dehaze, sharpening, dodge & burn, frequency separation, presets/batch, then RAW Β· denoise Β· upscale Β· lens correction