Skip to content

Add a one-stroke Remove tool - #206

Merged
IAmJSD merged 7 commits into
mainfrom
feat/remove-tool
Oct 4, 2026
Merged

IAmJSD merged 7 commits into
mainfrom
feat/remove-tool

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 4, 2026

Copy link
Copy Markdown
Member

What and why

This adds a Photoshop-style Remove tool to the healing group (J, Shift+J cycles to it; also in Spotlight). You paint over an unwanted object in one stroke and it is filled in when you release. Brush size is pressure-sensitive and works with [/]. Inference runs off the UI thread with a progress bar and Cancel, so painting is never blocked. The tool reuses the existing inpainting network and Content-Aware Fill pipeline; no new models are added.

Design

  • Plugin API (crates/plugin-api/src/background.rs). Adds BackgroundEdit, which has three stages:

    • prepare(&Document) runs on the UI thread and can be re-called.
    • run(&JobControl) runs on a worker. JobControl carries progress, cancellation and a missing-model notice.
    • apply(&mut Document) runs on the UI thread as one history entry.

    Tools get set_background_edits / take_background_edit. Hosts that never enable these (tests, MCP, the browser) run the edit inline. Also adds Overlay::Stroke for the translucent pink stroke.

  • Tool (plugins/tools-retouch/src/remove.rs).

    • Mask: the stroke is drawn as discs joined by tapered capsules (pressure), then grown by 10% of the radius (1–12 px) and feathered over 8% (1–6 px). Distances use an exact Euclidean distance transform.
    • Context window: the hole plus half its longer side (24–768 px), matching the network's training distribution and Content-Aware Fill. At image edges the window slides inwards instead of shrinking.
    • Small-stroke fallback: holes up to SPOT_HEAL_MAX_THICKNESS = 5.5 px thick use spot healing's ring interpolation, generalised to any hole shape. Thicker holes use the inpainting network, then patch synthesis, then seam relaxation.
    • Choosing 5.5 px: I measured it with examples/remove_eval.rs: 192 stroke holes across 8 Kodak images, scored on error and texture ratio. The table is in the doc comment. Below about 5.5 px, patch synthesis adds more edges than the photo had (texture ratio 1.2–1.7) and about 10% more error. Above it, the fill's texture ratio is about 1, while interpolation smears (about 0.25).
    • Fill entry point: fill.rs gains inpaint_window with cancellation and progress. inpaint() behaves as before.
    • GPU/CPU: the GPU path is whatever schist_neural already does; the backend is selected automatically, with CPU fallback.
    • Halo/seam handling: halo.rs is Anti-Smudge's glow cleanup and doesn't apply to inpainting. The seam handling used is Content-Aware Fill's own seam relaxation plus the feathered blend.
    • Model missing: the classical Content-Aware Fill path runs (diffusion seed, then patch synthesis) and the host is notified. Natively the model is built in, so this only happens if it fails to load. In the browser it happens until the model is fetched.
    • Options: "Sample All Layers" reads the composite. It writes to the active pixel layer, or to a new empty "Removed" layer if the active layer isn't one; creating the layer is part of the same undo step. "Remove After Each Stroke": when off, strokes accumulate and Enter or the options-bar Apply removes them in one fill; Escape discards them. An active selection limits what is written. Each removal is one undo step.
  • Editor (crates/editor/src/workspace/tool_jobs.rs).

    • Queued edits run one at a time on the background executor; new strokes queue behind the running one.
    • The status bar shows a spinner, percentage, progress bar and Cancel. Escape cancels first.
    • After a fill that ran without the model, the status bar offers Manage Models.
    • If the document changed mid-run (undo, another tool), the edit is re-prepared, up to 3 times. If the document was switched or closed, nothing is written.
    • The options bar shows Size (no Opacity), plus Apply when strokes are accumulating.
    • New icon icons/remove.svg, drawn in the line style of the other healing icons.
  • i18n: 9 new keys, translated in all 147 catalogs (split across 10 subagents); no/sh regenerated.

Before/after (eyeball these)

These are on the dev box under ~/remove-tool-demo/, not attached here.

  • k19-side.png (kodim19): the coin binocular viewer is removed. The fence and plinth behind it come back patchy and blurred, and the small DANGER sign inside the brush went with it.
  • k20-side.png (kodim20): a background plane at the image edge is removed. The fill is blocky where the hangar and tree line were.
  • k13-side.png (kodim13): a synthetic 1 px wire, removed with a 3 px brush (spot path). Clean over sky; over rock it leaves a visible smooth band.
  • Full frames: kodim{13-wire,19,20}-{before,after}.png.

Verification done

  • cargo test -p schist-tools-retouch -p schist-plugin-api: all pass. New tests cover:
    • stroke-to-mask geometry, pressure taper and distance-transform exactness
    • context-window cropping at image edges and corners
    • the small-stroke spot-heal fallback
    • the model-missing fallback and its notice
    • cancellation
    • one undo step per removal, including a created layer
    • accumulate mode with Enter/Escape
    • Sample All Layers into an empty layer, and creating a new layer
    • selection confinement
    • the background-host three-stage flow on a real worker thread
  • GPU vs CPU parity for the Remove fill (network + synthesis + seam) was added to compositor-gpu/tests/compute_programs.rs and passes on lavapipe.
  • cargo test -p schist-mcp passes.
  • cargo clippy -D warnings is clean on schist-tools-retouch, schist-plugin-api (all targets) and schist-editor.
  • cargo fmt was run.
  • make check-i18n passes. The web-font coverage check initially caught Tibetan 《》; they are replaced.

Not verified

  • No headless GUI run. Disk was tight (about 12–18 GB free, shared), so I didn't build the full app. I haven't seen these on screen: the status-bar progress and Cancel, the pink stroke overlay, the toolbar flyout position, the Apply button, and the Manage Models link. The editor compiles and passes clippy.
  • I haven't tested a real pen; pressure goes through BrushDynamics::pressure_size.
  • The browser build is untested; there the removal runs inline with no progress bar.
  • make check-neural-gpu wasn't run; only the targeted parity test was.
  • Low-resource translations are best effort. The subagents flagged ak, bm, bi, bo, ba, an, kw, lg, li, ln, mg, mh, gn, fo, ha, gd, tw, wa, wo, ve, xh, yo, zu, dv, dz, ee and sc/se/sm/sn/so/ss/st/su/sd/si for native review. fr uses "Supprimer"; Photoshop's French may be "Suppression".

Limits

🤖 Generated with Claude Code

IAmJSD and others added 7 commits October 4, 2026 10:05
BackgroundEdit splits an edit into prepare (UI thread), run (worker,
with JobControl progress, cancellation and a missing-model notice) and
apply (one history entry). Hosts that do not enable background edits get
them run inline. Adds Overlay::Stroke for translucent painted strokes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dings

The stroke becomes a grown, feathered mask; a context window sized to the
inpainting network's training slides inside the canvas; thin holes use
spot healing's ring interpolation (threshold measured by the new
remove_eval example) and the rest Content-Aware Fill's network + patch
synthesis + seam pipeline, now cancellable and reporting progress. Sample
All Layers, per-stroke or accumulated removal, selection confinement and
one undo step per removal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Queued tool edits run one at a time on the background executor; the
status bar shows progress with Cancel (Escape also cancels) and offers
Manage Models after a fill that had no network. Results whose document
changed are re-prepared. Options bar gets Size and Apply for the Remove
tool, the canvas draws pending strokes, and the tool gets its icon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD
IAmJSD merged commit 285a70c into main Oct 4, 2026
5 checks passed
@IAmJSD
IAmJSD deleted the feat/remove-tool branch October 4, 2026 12:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant