🎨 image-editorHomeUser GuidePro GuideInstallArchitectureAnalysisPlanChangelog

image-editor β€” Feature Research & Analysis

Research analysis for a new standalone photo-editing desktop app. Not an image selector β€” a plain editor with Photoshop-grade capabilities but phone-app-level ease of use: erase people, apply an effect to only part of a picture, blend the result so the edit is invisible, colour/tone curves β€” and save at full original resolution.

Date: 2026-09-11 Β· Status: research / pre-implementation


0. Product context & design principles

The problem that started this. Editing is currently done in phone apps, which destroy resolution: they downscale the working image and re-compress on export, so a 24 MP original comes back as a soft, recompressed few-megapixel JPEG. There is no way to do a capable edit and keep the original pixels.

So the two non-negotiables are:

  1. Full-resolution, non-destructive. The original is never downscaled. The preview may work on a scaled proxy for speed, but the final render always runs the edit pipeline at native resolution and saves with quality/format control (real JPEG quality, or PNG/TIFF). This is already how image_selector saves (full-res render only at save, atomic + timestamp-preserving) β€” we keep that and make it the headline feature. It is the single biggest advantage over any phone app.
  2. Phone-app-simple, Photoshop-capable. The power (masks, layers, inpainting, curves) lives underneath a UI that stays as approachable as Snapseed/Instagram: - one big image, direct-on-image gestures (pinch-zoom, brush, drag-to-adjust), - tap the person β†’ Erase, drag on the sky β†’ darker, not "create an adjustment layer, add a luminosity mask, set blend mode", - sensible auto-defaults (auto-feather, auto-pick inpaint engine, auto colour-match) so the simple path needs no jargon, and the depth is opt-in. - Snapseed is the model to study: pro results, thumb-friendly, no layer panel in the user's face β€” the layer/mask machinery is an implementation detail, not the interface.

Every feature section below should be read through this lens: the algorithm is the Photoshop half; the interaction design is the phone-app half, and both must ship together.


1. Why this is a new project (and not "just more OpenCV")

The features requested β€” erasing people, applying contrast/effects to a selected region only, blending an edit to match its surroundings, colour and tone curves β€” split into two very different tiers of difficulty:

Tier Technique Tooling Quality
Classical (OpenCV/NumPy) cv2.inpaint (Telea / Navier–Stokes), grey-world WB, LUT curves, cv2.seamlessClone Already in image_selector's edit_ops.py Fine for scratches, dust, tiny blemishes and simple gradients β€” falls apart on anything textured or larger than a few dozen pixels
Content-aware / ML LaMa / diffusion inpainting, SAM2 segmentation, UΒ²-Net matting, PatchMatch, gradient-domain blending New dependencies (PyTorch/ONNX + models) This is what "erase a person and you can't tell" actually requires

This is the earlier point about "OpenCV is too simple": cv2.inpaint diffuses neighbouring colour into the hole. It cannot hallucinate plausible texture (grass, brick, a face behind the removed person), so removing a whole person leaves a smear. The sophisticated behaviour you want is fundamentally a machine-learning problem sitting on top of an OpenCV/NumPy plumbing layer β€” not a pure-OpenCV one. So image-editor keeps OpenCV for the fast pixel math and adds a model runtime (ONNX Runtime, optionally PyTorch) for the hard parts.

The good news: the whole editing architecture from image_selector transfers directly β€” Qt-decoupled pure-array ops, an EditState, a fixed pipeline, atomic Unicode-safe saves. We extend that model from global state to masked, layered state.


2. The one foundation everything else needs: selections & masks

Every sophisticated feature below ("only on certain sections", "erase this person", "blend to match") is really "do operation X, but only where mask M says so, and feather the boundary". So the mask engine is the core of the app, not an afterthought. This is exactly how Darktable and Lightroom are built: a module does one thing globally, and a mask restricts where it applies.

How the reference tools do it

Implementation for image-editor


3. Erasing people & objects (inpainting)

Two sub-problems: (a) get the mask of the thing to remove, and (b) fill the hole convincingly.

(a) Getting the mask β€” segmentation

Approach What it is Library / model Notes
Manual brush user paints over the person our own mask engine (Β§2) always available, zero deps
SAM 2 (Segment Anything Model 2, Meta) click a point or drag a box β†’ pixel-perfect mask; foundation model, prompt-based (points/boxes/masks) ultralytics SAM-2 or segment-anything-2; ONNX export possible ~44 fps inference, real-time feedback; the gold standard for "click the person, get the mask" (SAM 2 Β· Ultralytics, SAM2 overview (arXiv))
UΒ²-Net / rembg whole-image foreground/background matting rembg (bundles u2net, u2net_human_seg, BRIA RMBG) via ONNX Runtime great for "remove the background", "cut out the subject"; u2net_human_seg is people-specific (rembg, UΒ²-Net explained)

Recommendation: ship SAM 2 (interactive, precise, this is what makes "erase that person" feel magic) + rembg for one-click subject/background cutouts, and always keep the manual brush as the dependency-free fallback.

(b) Filling the hole β€” inpainting

Method Engine When it wins Cost
Telea / Navier–Stokes cv2.inpaint 1–20 px defects: dust, scratches, sensor spots, tiny blemishes instant, CPU, already have it
PatchMatch ("content-aware fill" / GIMP Resynthesizer) classical, copies best-matching patches from elsewhere in the image textured but self-similar backgrounds (grass, gravel, water, sky) fast-ish, CPU, no model
LaMa (resolution-robust large-mask inpainting) iopaint / simple-lama-inpainting, ONNX or PyTorch the default for object/person removal β€” hallucinates plausible structure over large holes, matches surroundings ~100–500 ms GPU, seconds CPU; ~200 MB model
Stable-Diffusion inpaint iopaint with an SD model replace with new content ("erase and put grass"), outpainting/extending canvas heavy (GB-scale model, GPU strongly preferred)

How the reference tool (IOPaint, formerly Lama-Cleaner) works end-to-end: load image → paint a mask over the unwanted object → an erase model (LaMa) fills the region to match surrounding pixels; a diffusion model can instead replace or outpaint. This is exactly the mask→fill pipeline we build. (IOPaint on GitHub)

Recommendation: a single "Erase" action that picks the engine by mask size β€” cv2.inpaint for tiny masks, LaMa for anything person/object-sized β€” with SD inpaint as an optional "replace with…" mode behind a flag. IOPaint can even be vendored as a library rather than reimplemented.


4. Local adjustments β€” "contrast/effects on certain sections only"

This is Β§2 (masks) applied to the existing edit_ops.py operations. Nothing new in the math β€” the novelty is that each adjustment carries a mask and a blend mode.

Implementation for image-editor


5. Colour & tone curves

The classic diagonal-line control: input on X, output on Y; identity is the straight line; a curve above brightens, below darkens, and an S-curve adds contrast. Points are joined by a monotonic spline (not plain cubic, which overshoots and causes tonal reversals). (darktable β€” tone curve)

Key design decisions the reference tools make:

Implementation for image-editor


6. Blending the final picture to match (the "invisible edit")

Two distinct blending problems, often confused:

(a) Boundary blending β€” hide the seam of a paste/composite

Poisson image editing / gradient-domain blending (PΓ©rez, Gangnet & Blake, SIGGRAPH 2003): instead of copying pixel values across a boundary, it copies the source region's gradients and solves a Poisson equation so the pasted region's colours/brightness bend to meet the destination at the seam β€” the transition becomes invisible. This is precisely how you "blend the final picture to match". Directly available as cv2.seamlessClone(src, dst, mask, center, flags) with NORMAL_CLONE / MIXED_CLONE / MONOCHROME_TRANSFER modes. (OpenCV seamless cloning, Poisson image editing (PΓ©rez et al.), learnopencv walkthrough)

(b) Colour/tone matching β€” make the edited region belong

Even with a perfect seam, a pasted or inpainted region can be the wrong colour temperature or exposure. Colour transfer matches the statistics (mean + standard deviation per channel, classically in Lab space β€” Reinhard et al.) of the edit region to a reference region so its palette matches. Histogram matching (skimage.exposure.match_histograms) is the stronger variant.

Implementation for image-editor


7. Layers & non-destructive document model

To hold all of the above, the EditState from image_selector grows into a small document model (like a lightweight PSD / Darktable history stack):

Document
β”œβ”€β”€ base image (immutable original, BGR/float)
β”œβ”€β”€ ordered layer stack (bottom β†’ top)
β”‚   β”œβ”€β”€ AdjustmentLayer { op, params, mask, blend_mode, opacity, visible }
β”‚   β”œβ”€β”€ HealLayer       { mask, engine: telea|patchmatch|lama, params }
β”‚   β”œβ”€β”€ PixelLayer      { rgba, transform }          # pasted / composited content
β”‚   └── ...
β”œβ”€β”€ crop_rect, rotation                              # from existing pipeline
└── history (undo/redo of layer ops)

Rendering = start from base, apply each visible layer top-to-bottom via its mask + blend mode. Everything stays non-destructive and reproducible, matching the design goal image_selector already committed to (pure-array ops, full-res render only at save). Save-as remains atomic + Unicode-safe + timestamp-preserving, reusing img_io.py and the tempfile-then-shutil.move pattern.


Reuse everything proven in image_selector; add a model runtime.

Layer Choice Rationale
UI PySide6 / Qt (QPainter preview, zoom/pan) direct reuse of preview_widget.py, edit_panel.py, crop overlay
Pixel math OpenCV (+ opencv-contrib-python for ximgproc guided filter) + NumPy reuse edit_ops.py, film_luts.py, LUT/curve helpers
Curves monotone cubic spline β†’ LUT (scipy PchipInterpolator or hand-rolled) keep deps light
Segmentation SAM 2 (interactive) + rembg / UΒ²-Net (one-click cutout) precise masks; the "click the person" magic
Inpainting cv2.inpaint (tiny) β†’ LaMa (iopaint/simple-lama-inpainting, default) β†’ optional SD inpaint (replace/outpaint) right engine per mask size
Blending cv2.seamlessClone (Poisson) + Lab colour transfer / histogram match (scikit-image) invisible seams + colour match
Model runtime ONNX Runtime first (CPU-friendly, easy install); PyTorch only where a model needs it Mario's box may be CPU-only; ONNX keeps it usable
AI-assist (optional) keep the ai_edit.py Claude/Ollama bridge for text-driven global looks already built, complements manual local tools

GPU note: LaMa and SAM run on CPU (seconds) or GPU (sub-second). SD inpaint really wants a GPU. Structure model calls behind a backends/ interface so a missing/absent GPU degrades gracefully (LaMa-CPU, or fall back to PatchMatch).

Dependency & licence snapshot

Package Purpose Model size Licence notes
opencv-contrib-python pixel ops + guided filter + seamlessClone β€” Apache-2.0
onnxruntime run ONNX models on CPU/GPU β€” MIT
rembg background/subject cutout ~5–180 MB MIT (models vary β€” check BRIA RMBG terms)
SAM 2 (ultralytics or segment-anything-2) interactive segmentation ~40–350 MB check per-model (Apache/BSD; some non-commercial)
iopaint / simple-lama-inpainting LaMa erase (+ optional SD) LaMa ~200 MB LaMa weights: verify licence before commercial use
scikit-image histogram matching / colour transfer β€” BSD
scipy (optional) monotone spline for curves β€” BSD

⚠️ Licensing matters here given Mario keeps a day job and ships side projects publicly: several segmentation/inpainting weights carry non-commercial or attribution terms distinct from their code. Pin exact model versions and record each licence before shipping.


9. Suggested build order

  1. Document + mask engine + layer compositor (Β§2, Β§7) β€” the spine. Drawn masks (brush/gradient/shape) + feather + guided-filter edge-aware refinement.
  2. Local adjustments (Β§4) β€” wrap existing edit_ops in masked layers. Immediate payoff, no new heavy deps.
  3. Curves (Β§5) β€” master + per-channel, LUT-based.
  4. Classical heal/erase (Β§3b) β€” cv2.inpaint + PatchMatch for small stuff.
  5. Seamless paste + colour match (Β§6) β€” seamlessClone + Lab transfer.
  6. AI segmentation (Β§3a) β€” SAM 2 click-to-mask + rembg cutout, wired into the mask engine.
  7. LaMa erase (Β§3b) β€” the headline "remove a person" feature.
  8. (Optional) SD inpaint / outpaint, and the ai_edit text-prompt bridge for global looks.

Phases 1–5 are pure OpenCV/NumPy/Qt (no models, works everywhere). Phases 6–8 add the ML runtime β€” the part that makes edits genuinely sophisticated rather than simple.


10. What transfers directly from image_selector


11. Professional-grade capabilities β€” research (2026)

Added after a research pass on what actually makes photos look professional and which tools deliver it, to steer image-editor toward pro-quality output. The goal: keep the phone-simple UX while adding the levers that separate an amateur edit from a professional one.

What the pros actually do (the workflow)

The consistent professional pipeline, across guides, is an ordered, non-destructive workflow β€” not random slider-pushing:

  1. Cull ruthlessly (pick keepers; toss blurry/blinked).
  2. Base correction on the RAW β€” white balance, exposure, highlight/shadow recovery.
  3. Tone & colour β€” contrast/curves, then colour grading (HSL + split-tone).
  4. Local work β€” masked adjustments, dodge & burn to shape light.
  5. Retouch β€” heal/spot removal; frequency separation for skin.
  6. Denoise (especially high-ISO) and lens/perspective correction.
  7. Output β€” resize + output sharpening tuned for screen or print.
  8. Consistency β€” a signature look applied as presets across a set; increasingly an AI assistant trained on the photographer's style handles the mechanical steps. (Imagen β€” edit like a pro, Filterpixel β€” repeatable workflow, Fstoppers β€” freq. separation / dodge & burn / grading)

The "pro look" is mostly: accurate colour + white balance, deliberate light (dodge & burn), clean colour grading, natural skin, low noise, and correct sharpening β€” applied non-destructively and consistently.

The reference tools (and what each is for)

Tool Licence Known for Relevance to us
Adobe Lightroom Classic paid the default RAW workflow; GPU RAW; AI masks the workflow model to emulate
Capture One paid best colour tools & skin rendering, tethering its colour-grading depth is the bar for our HSL/colour work
DxO PhotoLab paid DeepPRIME denoise + best lens corrections denoise + lens-correction are its moat β€” both are gaps for us
Darktable FOSS scene-referred pipeline, 60+ modules, masks our closest open reference; algorithms we can mirror
RawTherapee FOSS powerful RAW demosaic & detail reference for RAW processing
Photoshop paid pixel retouching, frequency separation, dodge & burn the retouching techniques to implement
Topaz Photo AI paid denoise / sharpen / upscale / face recovery what "AI enhance" means to users
Real-ESRGAN / Upscayl FOSS AI super-resolution, rivals paid upscalers our open path to upscaling
(Best editing software 2026,
Topaz upscalers,
Real-ESRGAN)

image-editor today vs. the pro gap

Already have (good foundation): exposure/brightness/contrast/highlights/shadows, saturation/vibrance, temperature/tint, curves (master + RGB), local masked edits with feather + edge-aware borders, heal (classical + LaMa), remove-person (SAM), reshape/liquify, seamless paste, crop/rotate, 30 looks, full-res non-destructive, layers, .iedit projects. Dodge & burn is nearly free already (masked local exposure).

The gaps that most affect a professional result, high β†’ lower leverage:

  1. HSL + colour grading (per-hue sat/lum; shadow/mid/highlight colour balance / split-tone). Capture One's headline strength; the biggest "pro colour" lever we lack. (OpenCV/NumPy β€” no model.)
  2. White-balance eyedropper + Kelvin temp; auto-WB we have, but not click-neutral. (easy)
  3. Clarity / Texture / Dehaze (local-contrast midtone punch; dark-channel-prior haze removal). (OpenCV β€” no model.)
  4. Sharpening β€” capture sharpening (unsharp/high-pass) and output sharpening sized for screen/print. A pro final step we don't have. (OpenCV β€” no model.)
  5. Dodge & burn tool β€” a dedicated brush (leverages our masked-exposure layers). (reuse.)
  6. Denoise β€” classical (cv2.fastNlMeansDenoisingColored) now; AI denoise later. High-ISO cleanup is a top pro differentiator (DxO DeepPRIME). (classical easy; ML bigger.)
  7. Frequency separation β€” smooth skin tone while keeping texture, for portraits. (low/high-pass split β€” OpenCV.)
  8. Vignette + film grain β€” finishing touches for a graded look. (easy.)
  9. Presets β€” save an adjustment recipe (an .iedit minus the pixels) and apply it to any image / a batch. Consistency is itself professionalism. (reuses our serializer.)
  10. RAW input (rawpy/libraw) β€” the single biggest image-quality gap: real highlight recovery and WB come from RAW, not JPEG. (dependency, no model.)
  11. Lens & perspective correction β€” distortion/vignetting/chromatic-aberration and keystone/horizon straighten. (OpenCV undistort / perspective; manual sliders feasible.)
  12. AI super-resolution / upscale β€” enlarge for print without going soft; Real-ESRGAN via ONNX Runtime is the open path (reuses our lazy-backend pattern from LaMa/SAM). (model.)

Recommendation

Most of the "professional look" is reachable with no new dependencies β€” items 1–5, 7–9 are pure OpenCV/NumPy and slot into the existing layer/mask/curve engine. Items 6, 10–12 (AI denoise, RAW, lens profiles, super-resolution) are the heavier, higher-quality tier that follows the same optional-backend pattern already proven with LaMa and SAM. Until RAW/denoise/upscale land, pairing image-editor with a free RAW processor (Darktable / RawTherapee) covers the gap.

See plan.md Phase 9 for the prioritized build.


Sources