Compose a series of monotonic piecewise functions and a color transfer function into a single color transfer function#3531
Conversation
699fd8a to
7b3f85c
Compare
|
Did you check ColorTransferFunction/CssFilters.js ? Maybe it already does what you need... |
7b3f85c to
d75d6a0
Compare
df5aea4 to
4644df0
Compare
| //---------------------------------------------------------------------------- | ||
| publicAPI.getDataPointer = () => { | ||
| return model.nodes; | ||
| }; |
There was a problem hiding this comment.
@finetjul Is it ok to return a direct reference to model.nodes or should we make a copy of the array?
There was a problem hiding this comment.
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
4644df0 to
15b4364
Compare
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. |
4a2b612 to
13545e5
Compare
Indeed, what you want to do is non-linear. Those 5D matrices won't work for your use case. Nevermind |
|
@jadh4v please let us know when it is a good time for us to do a code review. |
05b3c3f to
af9d3da
Compare
@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. |
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>
af9d3da to
fa3dd36
Compare
| let lo = x0; | ||
| let hi = x1; | ||
| let mid = 0.5 * (lo + hi); | ||
| while (lo < mid && mid < hi) { |
There was a problem hiding this comment.
Should there be another stop condition (like an epsilon) for near-flat slopes?
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.
Changes
vtkPiecewiseFunction.findX(y)(new method): inverse ofgetValue(). Locates the first segment whose endpoint values bracketyand bisects it through forward evaluation, so midpoint and sharpness are honored. Returnsnullwhen noxmaps toy(e.g. out-of-range values with clamping off).compose(fnList, colorFn, outputFn, errorThreshold?, maxSubdivisionDepth?)(new helper inCommon/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.getType() !== 'Varied') so breakpoints can be pulled back unambiguously. Without anerrorThresholdthey must also be piecewise linear (midpoint 0.5, sharpness 0 on every segment), since the output interpolates linearly between the collected breakpoints; with anerrorThreshold, 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.errorThresholdmeans the depth cap stopped refinement somewhere), ornullwhen an input is rejected (with a warning, leavingoutputFnuntouched). Note that 0 is falsy: test the result againstnullfor success, never its truthiness.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.vtkPiecewiseFunctiontypings fixed:getNodeValue/setNodeValuewere declared to returnvoidbut return-1(index out of range) or1(success);valnarrowed fromany[]tonumber[].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
npm run reformatto have correctly formatted codeTesting