Skip to content

feat(canvas): Clarity, Texture and Dehaze adjustments on layers and groups - #10001

Merged
joshistoast merged 28 commits into
mainfrom
feat/canvas-spatial-adjustments
Oct 24, 2026
Merged

joshistoast merged 28 commits into
mainfrom
feat/canvas-spatial-adjustments

Conversation

@joshistoast

@joshistoast joshistoast commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds Clarity, Texture and Dehaze as non-destructive adjustments on raster layers and raster groups, using the same kernels as the backend nodes from #9891.

  • Matches the nodes: results are byte-identical to the backend nodes run on the layer's alpha-trimmed pixels, so they don't depend on the bbox or crop.
  • Off the main thread: a background worker computes them. While a job runs, the canvas shows only the colour adjustments (an interim) and then installs the exact result. A layer row shows a delayed spinner while computing, or a warning when an adjustment can't run (over 16.8 MP, over the memory budget, or a worker error).
  • Exact or refused: generation, export, gallery save, Merge Down, rasterize, PSD export, extract, filter and Select Object inputs, crop, copy and painting all either use the exact adjusted pixels or refuse with a message naming the layer and the reason. A spatial adjustment is never silently dropped.
  • Settings: Amount runs from −200 to 200. Size uses a log scale: 4–1000 px for Clarity, 1–100 px for Texture.

Shares canvas schema v5 with the Noise adjustment (#9933).

Related Issues / Discussions

Closes #9813
Builds on #9891 (backend nodes) and #9933 (Noise adjustment, schema v5).

QA Instructions

Automated (Node 22.14, pnpm 10):

  • pnpm check:release passes: lint, 11,179 unit tests, 3,345 browser tests, fixtures, both performance gates, the project-file journey and accessibility journeys.
  • Python: pytest tests/backend/image_util passes, including the shared parity fixture that pins the TS kernels to the Python output.

Manual, on the mock backend:

  1. Add a raster layer and paint on it. Right-click → Add adjustment → Clarity, Texture or Dehaze.
  2. Scrub Amount: the canvas updates within about 0.1 s at 1024². For example, Dehaze +200 lifts a 127 grey to 136, which matches the worker output.
  3. Toggle the entry off and on: pixels return to the original values and back.
  4. Reload: the entries persist (the document is written as v5).

Not yet done: a real-backend comparison of a canvas result with the node on the same alpha-trimmed image.

Screenshots, on the mock backend:

Add adjustment menu on a raster layer:

Layer context menu with Add adjustment open, listing Texture, Clarity, Dehaze and Noise

Texture +80, Clarity +120 and Dehaze +60 on one layer, with Clarity selected:

Layer with Texture, Clarity and Dehaze rows; Clarity Amount and Size settings

Dehaze settings and hint:

Dehaze Amount setting with its approximation hint

Before and after, with the three adjustments off, then on:

The same photo without and with the three adjustments

Row status: the spinner while a job runs on a 3072² layer, and the warning with its tooltip on a 4100² layer:

Layer row with a spinner while the adjustment computes Layer row warning and tooltip for content over 16.8 megapixels

Review

Resolved findings:

  • Cooldown cancellations: cooldown no longer cancels the exact jobs that generation waits on.
  • Painting refusals: painting on a layer whose adjustment can't run now gets a specific message.
  • Size-limit retries: an oversized layer no longer retries on every slider tick.
  • Group status: a group's status no longer flips between the canvas and overview reads.
  • Stale warnings: a hidden layer's warning clears once its adjustment is removed.

Known limits:

  • Offscreen thumbnails: offscreen layers that have never been drawn show only the colour adjustments in their thumbnail.
  • PSD groups: PSD export approximates spatial group stacks per layer (documented).
  • Previews in groups: while a filter preview or floating selection sits inside a spatial group, the canvas and the overview each compute the group.
  • Large layers: layers over 16.8 MP are refused; tiled evaluation is a follow-up.

Compatibility / Rollout

  • Schema: documents holding any of the three adjustments, enabled or not, save as canvas schema v5. This is shared with Noise (feat(canvas): noise adjustment for film grain #9933), so older builds refuse those documents instead of silently dropping the adjustment stack. Documents without them keep their current version.

  • Bundle size: measured against feat(canvas): noise adjustment for film grain #9933's build:

    • about +22 KB raw startup script on editor routes and +30 KB on canvas;
    • +2.4 KB of locale strings;
    • +7.7 KB when the canvas widget opens;
    • no new requests.

    The browser baseline re-records only byte figures and startup-module lists; timings are unchanged. The kernels load lazily in a 9 KB worker chunk.

  • Memory: worker scratch (at most about 400 MB, one job at a time per engine) is outside the raster memory budget. The pixel buffer each job uses is reserved.

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Meaningful regression coverage added / updated where needed; obsolete tests/code removed
  • Persisted-state and API changes include required migrations / compatibility validation
  • Relevant performance/efficiency opportunities considered; material claims have evidence
  • Material review findings resolved and relevant checks rerun
  • Documentation added / updated (if applicable)
  • Updated What's New copy (if doing a release after this PR)

joshistoast and others added 19 commits October 18, 2026 22:40
Adds a Noise raster/group adjustment with Gaussian or Uniform distribution, monochromatic or color grain, amount, and a re-rollable seed.
Noise is a deterministic function of position, so display, generation, export, merges and PSD share the same grain.
Layer bakes and PSD now apply a layer's own adjustments before resampling, matching the display.
Noise gains a 1–8 px grain size (coordinate-pure Gaussian-correlated samples), a midtones response that fades
grain to none at black and white, and a chroma grain on OKLab a/b that keeps lightness.
Re-records the editor source-owner set and canvas route bytes for the larger kernel.
Any noise adjustment, enabled or not, raises the canvas and the project floor to 5 and never lowers it,
so older builds refuse the project instead of dropping the adjustment stack and saving the loss.
Merge Down bakes both layers' stacks layer-local before compositing; Rasterize bakes a noise stack before the
transform and drops it. Composites reserve measured text scratch past its estimate or refuse as over budget.
Covers grain size, tonal response and chroma, how Merge Down and Rasterize bake adjustments, the noise kernel's
cost and region-independence invariants, and that older versions refuse projects with noise.
…ustment

# Conflicts:
#	invokeai/frontend/webv2/performance/browser-baseline.json
#	invokeai/frontend/webv2/src/workbench/projects/projectFile.test.ts
A legacy project kept whole because its workflow repeats an item id now sends the raised canvas floor like split
saves do, so saving Noise into it stores floor 5 and older clients refuse it.
Export reuses the display's adjusted copy only when it is current and never builds a layer-sized one, so a cache miss adjusts just the reserved sampled region.
Add Clarity, Texture, and Contrast Dehaze approximation invocations with shared numerical primitives, schemas, tests, and docs.
Allow Clarity, Texture, and Contrast Dehaze amounts from -200 to 200.
…ab restore

Restore Oklab lightness in float64 per tile and gamut-clip only pixels more than 1e-5 out of range, so float32 round-trip noise no longer shifts colours with a 0 or 255 channel by up to 45 levels.
…backend nodes

Port the nodes' numerics into the canvas engine (alpha-bounds domain, OpenCV chamfer fill, guided-filter bands, Oklab with gamut clip) and replace the unused Generation primitives.
A Python-generated parity fixture pins both sides byte for byte; provenance records the OpenCV and Oklab sources.
Add a lazily created module worker and a job service with priorities, same-slot replacement, abort, memory reservations and crash recovery. Nothing renders through it yet.
…schema v5

Adds the three adjustment entries to raster layer and group stacks. Documents holding any of them, enabled or not, write schema v5 so older builds refuse them instead of dropping the stack.
…he worker

Layers and groups show a per-pixel interim while a job runs and install only exact results. A keyed status store reports computing, too-large, over-budget and error per owner.
…djustments

Every pixel-writing operation evaluates spatial stacks exactly in the worker or refuses with a message naming the layer and the cause. PSD export approximates spatial group stacks per layer.
Adds the menu entries, Amount and Size controls, and a row indicator for pending or failed adjustments, with user docs.
@github-actions github-actions Bot added CI-CD Continuous integration / Continuous delivery python PRs that change python files invocations PRs that change invocations backend PRs that change backend files frontend PRs that change frontend files python-tests PRs that change python tests docs PRs that change docs labels Oct 23, 2026
Main's growth and the Noise adjustment together exceed the 1% byte allowance on the editor build and on canvas activation.
Base automatically changed from feat/canvas-noise-adjustment to main October 23, 2026 17:13

@JPPhoto JPPhoto left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge blockers:

  1. Must fix - Raster-to-control conversion and copy drop enabled spatial adjustments. invokeai/frontend/webv2/src/workbench/widgets/layers/layerOps.ts:483 clones the raw source for control layers. The context menu reaches this path for both conversion and copy at invokeai/frontend/webv2/src/workbench/widgets/layers/LayerContextMenu.tsx:473 and :730. A converted layer loses its adjustment stack; a copied control layer receives unadjusted pixels. Bake the exact result or refuse the action when evaluation fails. If skipped, control inference can use pixels different from the displayed layer. This happens whenever an affected layer has an enabled spatial adjustment. Conversion can be undone, but generated results from the copied pixels must be rerun. Add coverage that asserts exact pixels or refusal for both actions.

Other findings/issues:

  1. Should fix - The parity test does not enforce the byte-identical claim. invokeai/frontend/webv2/src/workbench/canvas-engine/render/spatial/evaluateSpatialSteps.test.ts:51 allows up to 1 byte of difference and up to 1% differing visible channels at lines 57-58. Tighten the assertion to exact bytes, or revise the claim to state the accepted tolerance. Otherwise small output drift can pass while the PR still claims exact backend parity. Add a test that enforces the chosen contract.

… byte

The parity test now requires exact visible bytes; every fixture case already matched.
…s adjustments

Copy and convert to control bake the exact adjusted pixels, or refuse naming the layer when the stack cannot run.
Convert is one undo step that restores the raster layer, its stack and its pixels; unadjusted and empty layers clone as before.
@joshistoast
joshistoast requested a review from JPPhoto October 23, 2026 21:21

@JPPhoto JPPhoto left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good!

@joshistoast
joshistoast enabled auto-merge (squash) October 24, 2026 00:57
@joshistoast
joshistoast merged commit bd7dde1 into main Oct 24, 2026
22 checks passed
@joshistoast
joshistoast deleted the feat/canvas-spatial-adjustments branch October 24, 2026 01:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend PRs that change backend files CI-CD Continuous integration / Continuous delivery docs PRs that change docs frontend PRs that change frontend files invocations PRs that change invocations python PRs that change python files python-tests PRs that change python tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[enhancement]: Adjustment layers: Vibrance/Saturation and Clarity/Dehaze

2 participants