Skip to content

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

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

Compose a series of piecewise functions and a color transfer function into a single color transfer function#3531
jadh4v wants to merge 5 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 us to push the color transformations directly to the GPU shader where the pixel values will transform to color values just-in-time to render, rather than storing them 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

  • 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 and others added 3 commits June 10, 2026 14:12
…ple piecewise functions

This example tests a feature that allows composition of multiple piecewise functions
into a single function which results in a single color transfer function.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move the piecewise function composition logic out of the
ComposePiecewiseFunctions example into a reusable, exported
compose() helper in Common/DataModel/PiecewiseFunction/helpers.js.

Add findX() as a public method on vtkPiecewiseFunction, replacing
the example local invertFn implementation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cover vtkPiecewiseFunction.findX (linear interpolation, multi-segment,
flat-segment handling, decreasing functions, clamping, empty function)
and Common/DataModel/PiecewiseFunction/helpers.compose (identity
passthrough, breakpoint inversion, multi-stage chaining, repeated calls).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jadh4v jadh4v self-assigned this Jun 10, 2026
jadh4v and others added 2 commits June 10, 2026 17:48
compose() no longer needs an explicit dataRange since outputFn's
mapping range is already derived from its node positions via
addRGBPoint's sortAndUpdateRange. Update the example and tests to
match the new compose(fnList, colorFn, outputFn) signature.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@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
// UI helpers
// ----------------------------------------------------------------------------

const body = document.querySelector('body');

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.

We do use lil-gui now, check other examples

if (model.clamping && nodes.length > 0) {
const first = nodes[0];
const last = nodes[nodes.length - 1];
if (y <= first.y) {

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.

If your piecewise function is a "decreasing function", you won't have the expected result here. Maybe you could:

  • check what y is higher between the first and the last point
  • or better: check the tangent at the first/last points.

(please create a unit test to enforce this scenario)

for (let i = 0; i < nodes.length - 1; i++) {
const { x: x0, y: y0 } = nodes[i];
const { x: x1, y: y1 } = nodes[i + 1];
if (y0 === y1) {

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.

you claim that you will return the "first" maching one, but if y is == y0, then you won't return the first matching x.
(Please create a unit test to check this scenario.)

const ret = [];
const v = [0, 0, 0, 0, 0, 0]; // [x, r, g, b, midpoint, sharpness]
for (let i = 0; i < cfun.getSize(); i++) {
if (cfun.getNodeValue(i, v) === 1) {

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.

can you please take the opportunity to fix index.d.ts to document the return value type of getNodeValue

@@ -0,0 +1,67 @@
function getColorFunctionXValues(cfun) {
const ret = [];

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.

for performance reason, you could allocate it to the size of cfun.getSize()

const ret = [];
const v = [0, 0, 0, 0, 0, 0]; // [x, r, g, b, midpoint, sharpness]
for (let i = 0; i < cfun.getSize(); i++) {
if (cfun.getNodeValue(i, v) === 1) {

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.

for performance reason, I wouldn't bother check the return value...

const minY = Math.min(y0, y1);
const maxY = Math.max(y0, y1);
if (y >= minY && y <= maxY) {
return x0 + ((y - y0) / (y1 - y0)) * (x1 - x0);

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.

here you ignore midpoint and sharpness and assume it is linear but it may not

// Also reverse compute from x-values of the final-stage color transfer function,
// and add those to our xSet so that we don't miss any break points defined
// within the color transfer function.
const colorXs = getColorFunctionXValues(colorFn);

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.

why not create a getDataPointer convenient method for a color function ? that would bring some consistency...

* outputs through to the color function, producing equivalent break points
* in the composed result. h(g(f(x))): g's x-values live in f's output domain
* and must be pulled back through f-inverse; h's x-values need g-inverse then
* f-inverse, and so on.

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.

you assume piecewise functions to be linear between nodes, but they may not.

@finetjul

Copy link
Copy Markdown
Member

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

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.

3 participants