Skip to content

Compose a series of monotonic piecewise functions and a color transfer function into a single color transfer function#3531

Open
jadh4v wants to merge 6 commits into
Kitware:masterfrom
jadh4v:feat-compose-piecewise-functions
Open

Compose a series of monotonic piecewise functions and a color transfer function into a single color transfer function#3531
jadh4v wants to merge 6 commits into
Kitware:masterfrom
jadh4v:feat-compose-piecewise-functions

Conversation

@jadh4v

@jadh4v jadh4v commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Context

Imaging applications may require multiple scalar transforms on pixel data before it is rendered. These transforms can be done directly on the image pixel array before passing the image to the GPU. However, the transform can necessitate a data type conversion to higher number of bytes to maintain precision. This can consume a lot of memory and make pixel arrays very large if the application is handling many such images simultaneously.

We implement a feature to compose multiple scalar transforms (piecewise functions) and one color transfer function into a single color transfer function that represents the entire transform from original input scalars to output display colors. This allows the transforms to be applied on the GPU via the composed transfer function's lookup at render time, rather than storing the transformed pixel arrays inflated on the CPU side.

Results

We have added an example that shows how three different scalar transforms (e.g. modality transforms, values-of-interest, color-window adjustments, etc) can be composed along with a color function.

image

Changes

  • vtkPiecewiseFunction.findX(y) (new method): inverse of getValue(). Locates the first segment whose endpoint values bracket y and bisects it through forward evaluation, so midpoint and sharpness are honored. Returns null when no x maps to y (e.g. out-of-range values with clamping off).

  • compose(fnList, colorFn, outputFn, errorThreshold?, maxSubdivisionDepth?) (new helper in Common/DataModel/PiecewiseFunction/helpers): composes an ordered chain of piecewise value-transform functions with a final-stage color transfer function into a single output color transfer function. Breakpoints from every stage are pulled back through the upstream inverses (findX) into the source domain, then forward-propagated to sample the composed color at each one.

    • Transform functions must be monotonic (getType() !== 'Varied') so breakpoints can be pulled back unambiguously. Without an errorThreshold they must also be piecewise linear (midpoint 0.5, sharpness 0 on every segment), since the output interpolates linearly between the collected breakpoints; with an errorThreshold, shaped (non-linear) segments in any stage are captured instead by recursively subdividing each output segment at its midpoint until linear interpolation matches the true composed color to within the threshold on every color channel. The subdivision is depth-first, so the threshold alone cannot bound the node count: maxSubdivisionDepth (default 24) caps the worst case at 2^depth − 1 extra nodes per segment and guarantees termination at discontinuities. The color transfer function itself is never restricted.
    • Returns the maximum measured midpoint deviation between the composed output and the true composed chain (0 means exact; a value above errorThreshold means the depth cap stopped refinement somewhere), or null when an input is rejected (with a warning, leaving outputFn untouched). Note that 0 is falsy: test the result against null for success, never its truthiness.
    • Usage: compose([modalityFn, voiFn, userFn], colorFn, resultFn, 0.01) — see the new example.
  • vtkColorTransferFunction.getDataPointer() (new method): returns the function's nodes, for parity with the VTK/C++ API.

  • vtkPiecewiseFunction typings fixed: getNodeValue/setNodeValue were declared to return void but return -1 (index out of range) or 1 (success); val narrowed from any[] to number[].

  • New example Rendering/ComposePiecewiseFunctions: DICOM slice viewer chaining modality, VOI, and interactive window/level transforms with a color map preset into a single transfer function.

  • Documentation and TypeScript definitions were updated to match those changes

PR and Code Checklist

  • semantic-release commit messages
  • Run npm run reformat to have correctly formatted code

Testing

  • This change adds or fixes unit tests
  • Tested environment:
    • vtk.js: latest master
    • OS: macOS 26.5.1 (25F80) Tahoe
    • Browser: Chrome Version 149.0.7827.55 (Official Build) (arm64)

@jadh4v jadh4v self-assigned this Jun 10, 2026
@jadh4v
jadh4v force-pushed the feat-compose-piecewise-functions branch from 699fd8a to 7b3f85c Compare June 10, 2026 22:05
@jadh4v
jadh4v requested review from finetjul and sankhesh June 10, 2026 22:06
Comment thread Examples/Rendering/ComposePiecewiseFunctions/index.js
Comment thread Sources/Common/DataModel/PiecewiseFunction/index.js Outdated
Comment thread Sources/Common/DataModel/PiecewiseFunction/index.js Outdated
Comment thread Sources/Common/DataModel/PiecewiseFunction/helpers.js Outdated
Comment thread Sources/Common/DataModel/PiecewiseFunction/helpers.js Outdated
Comment thread Sources/Common/DataModel/PiecewiseFunction/helpers.js Outdated
Comment thread Sources/Common/DataModel/PiecewiseFunction/index.js Outdated
Comment thread Sources/Common/DataModel/PiecewiseFunction/helpers.js
Comment thread Sources/Common/DataModel/PiecewiseFunction/helpers.js Outdated
@finetjul

Copy link
Copy Markdown
Member

Did you check ColorTransferFunction/CssFilters.js ? Maybe it already does what you need...

Comment thread Examples/Rendering/ComposePiecewiseFunctions/index.js
Comment thread Sources/Common/DataModel/PiecewiseFunction/helpers.js
Comment thread Sources/Common/DataModel/PiecewiseFunction/test/testHelpers.js
@jadh4v
jadh4v force-pushed the feat-compose-piecewise-functions branch from 7b3f85c to d75d6a0 Compare July 16, 2026 14:30
@jadh4v jadh4v changed the title Compose a series of piecewise functions and a color transfer function into a single color transfer function Compose a series of monotonic piecewise functions and a color transfer function into a single color transfer function Jul 16, 2026
@jadh4v
jadh4v force-pushed the feat-compose-piecewise-functions branch from df5aea4 to 4644df0 Compare July 16, 2026 20:02
Comment on lines +1277 to +1280
//----------------------------------------------------------------------------
publicAPI.getDataPointer = () => {
return model.nodes;
};

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@finetjul Is it ok to return a direct reference to model.nodes or should we make a copy of the array?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In VTK.JS world, it can be ok (just for performance reasons), but it is done with 2 separate methods: the default one returns a copy and another method with the same name but suffixed with "ByReference" returns a direct reference. (maybe add a warning in the documentation to not modify the array). You can look at the getArray function in macros.js

@jadh4v
jadh4v force-pushed the feat-compose-piecewise-functions branch from 4644df0 to 15b4364 Compare July 16, 2026 20:49
@jadh4v

jadh4v commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator Author

Did you check ColorTransferFunction/CssFilters.js ? Maybe it already does what you need...

Interesting. These filters look like direct color transforms (i.e. transforming 5d color vectors [R, G,B, A, 1] whereas we are working mostly with scalar value functions/transforms that are not restricted to linear matrix transforms. If I understand correctly the CssFilters transforms are entirely represented using 5D matrices or a composition of them.

@jadh4v
jadh4v force-pushed the feat-compose-piecewise-functions branch from 4a2b612 to 13545e5 Compare July 17, 2026 01:08
@finetjul

Copy link
Copy Markdown
Member

Did you check ColorTransferFunction/CssFilters.js ? Maybe it already does what you need...

Interesting. These filters look like direct color transforms (i.e. transforming 5d color vectors [R, G,B, A, 1] whereas we are working mostly with scalar value functions/transforms that are not restricted to linear matrix transforms. If I understand correctly the CssFilters transforms are entirely represented using 5D matrices or a composition of them.

Indeed, what you want to do is non-linear. Those 5D matrices won't work for your use case. Nevermind

@finetjul

Copy link
Copy Markdown
Member

@jadh4v please let us know when it is a good time for us to do a code review.

@jadh4v jadh4v changed the title Compose a series of monotonic piecewise functions and a color transfer function into a single color transfer function DRAFT: Compose a series of monotonic piecewise functions and a color transfer function into a single color transfer function Jul 17, 2026
@jadh4v
jadh4v marked this pull request as draft July 17, 2026 16:55
@jadh4v
jadh4v force-pushed the feat-compose-piecewise-functions branch from 05b3c3f to af9d3da Compare July 17, 2026 20:35
@jadh4v
jadh4v marked this pull request as ready for review July 17, 2026 20:36
@jadh4v jadh4v changed the title DRAFT: Compose a series of monotonic piecewise functions and a color transfer function into a single color transfer function Compose a series of monotonic piecewise functions and a color transfer function into a single color transfer function Jul 17, 2026
@jadh4v
jadh4v requested review from finetjul and sankhesh July 17, 2026 20:37
@jadh4v

jadh4v commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator Author

@jadh4v please let us know when it is a good time for us to do a code review.

@finetjul @sankhesh The PR is ready for re-review. I have made some significant changes to my previous approach. Primarily taking an iterative numerical approach for computing inverse as well as additional bisection of composed function.

jadh4v and others added 5 commits July 17, 2026 18:50
findX(y) returns an x such that getValue(x) === y. It locates the
first segment whose endpoint values bracket y and bisects it through
forward evaluation, so midpoint and sharpness are honored without
duplicating their math; within a segment the curve is always monotonic
between its endpoint values, so bisection converges. Flat (plateau)
segments need no special casing: a plateau value matches the endpoint
check and returns the plateau's left node (an equally valid inverse).
Discontinuous segments (sharpness ~1) return the jump location. For y
outside the output range with clamping on, the endpoint holding the
nearest extremum is returned; null is returned when no x qualifies
(extremum only attained at an interior node, clamping off, or empty
function).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add vtkColorTransferFunction.getDataPointer() returning the function's
nodes, mirroring the VTK/C++ API. The returned array is a live
read-only reference to the internal nodes, documented as such: direct
writes bypass sortAndUpdateRange() and modified(), so changes would
not propagate through the pipeline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ains

compose(fnList, colorFn, outputFn, errorThreshold?, maxSubdivisionDepth?)
composes an ordered chain of monotonic piecewise value-transform
functions with a final-stage color transfer function into a single
output color transfer function. Breakpoints from every stage (and from
the color function) are pulled back through the upstream inverses into
the source domain, then forward-propagated to sample the composed
color at each one.

Transform functions must be monotonic so breakpoints can be pulled
back unambiguously: any input whose getType() is 'Varied' aborts the
composition. Without an errorThreshold the output interpolates
linearly between breakpoints, so transform functions must additionally
be piecewise linear (midpoint 0.5, sharpness 0; the last node is
exempt since its parameters shape a transition to a node that does not
exist). With an errorThreshold, shaped segments in any stage are
captured instead by recursively subdividing each output segment at its
midpoint until linear interpolation matches the true composed color to
within the threshold on every channel. The subdivision is depth-first,
so the threshold alone cannot bound the node count; maxSubdivisionDepth
(default 24) guarantees termination at discontinuities and caps the
worst case at 2^depth - 1 extra nodes per segment. The color transfer
function is never restricted: its breakpoint colors are always exact,
and its shaped segments are approximated when a threshold is given or
linearized otherwise.

Returns the maximum measured midpoint deviation between the composed
output and the true composed chain (0 means exact; a value above
errorThreshold means the depth cap stopped refinement), or null when
an input is rejected, leaving outputFn untouched. 0 is falsy: callers
must test the result against null, never its truthiness.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 'Unknown (Error condition)' entry was carried over from the
VTK/C++ header, but in this implementation functionType can only be
0-3 and the default case returns 'Varied'; the .d.ts union already
reflects that. Note the divergence from C++ instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ctions

DICOM slice viewer demonstrating the DICOM value transform pipeline:
modality, VOI, and interactive window/level transforms are stored as
vtkPiecewiseFunction instances and chained with a color map preset via
the PiecewiseFunction compose() helper into a single RGB transfer
function applied to the image slice. Files are decoded with itk-wasm
loaded on demand; the transform parameters are driven by lil-gui
controls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
getNodeValue was declared to return void but the implementation
returns -1 (index out of range) or 1 (success); type both node
accessors as -1 | 1, narrow val from any[] to number[], and document
the return values.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jadh4v
jadh4v force-pushed the feat-compose-piecewise-functions branch from af9d3da to fa3dd36 Compare July 17, 2026 22:52
let lo = x0;
let hi = x1;
let mid = 0.5 * (lo + hi);
while (lo < mid && mid < hi) {

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.

Should there be another stop condition (like an epsilon) for near-flat slopes?

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.

4 participants