Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/transfer function shader widget #14

Closed
wants to merge 65 commits into from
Closed
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
379febd
feat: transfer function widget
seankmartin Jan 31, 2024
5927e34
refactor: fix linting warnings
seankmartin Jan 31, 2024
5ae8543
Merge branch 'master' into feature/transfer-function-shader-widget
seankmartin Feb 7, 2024
7b427f2
refactor: TF uses uint64 as number, not string
seankmartin Feb 7, 2024
c2000d1
chore: cleanup TODO and comments
seankmartin Feb 7, 2024
2e356a8
refactor: clean up transfer function code a little
seankmartin Feb 7, 2024
eebec72
fix: TF control points didn't always make it to JSON
seankmartin Feb 8, 2024
c8986c0
refactor: clearer comments and update loop
seankmartin Feb 13, 2024
5101386
fix: no longer able to place control points on top of eachother
seankmartin Feb 13, 2024
53a578b
fix: can grab control points in TF that are close in X by breaking ti…
seankmartin Feb 13, 2024
2aa0561
fix: bind remove TF point to shift+dblclick
seankmartin Feb 13, 2024
49466b2
feat: clearer name of TF input value
seankmartin Feb 16, 2024
1b142e5
feat: Python control over transfer function shader contro
seankmartin Feb 16, 2024
36ca253
docs: fix typo in python docs
seankmartin Feb 16, 2024
764dfdb
test (Python): shader control transfer function test
seankmartin Feb 16, 2024
12069f0
fix: user can't specify transfer function points outside input range
seankmartin Feb 16, 2024
3a2edd0
docs: transfer function UI control
seankmartin Feb 16, 2024
efe597d
Merge branch 'master' into feature/transfer-function-shader-widget
seankmartin Feb 20, 2024
abcfa33
docs: code comment on transfer functions
seankmartin Feb 27, 2024
d166378
Merge branch 'master' into feature/transfer-function-shader-widget
seankmartin Mar 19, 2024
a76cd33
chore: format and lint
seankmartin Mar 19, 2024
880e1b6
chore(python): format
seankmartin Mar 19, 2024
9363963
refactor(in progress): store control points in abs value
seankmartin Mar 19, 2024
3efed26
refactor(progress): transfer functino
seankmartin Mar 19, 2024
8b26b63
progress(refactor): tf refactor
seankmartin Mar 20, 2024
5f7e0c2
progress(refactor): tf refactor
seankmartin Mar 21, 2024
2d36889
progress(refactor): fix all type errors in tf file
seankmartin Mar 21, 2024
1225170
progress(refactor): fix type errors
seankmartin Mar 21, 2024
710cd39
refactor(progress): fix more type errors
seankmartin Mar 21, 2024
e661c35
fix(tests): remove unused imports
seankmartin Mar 21, 2024
c40483c
tests(fix): browser test whole TF
seankmartin Mar 21, 2024
fc06afc
Merge branch 'master' into feature/transfer-function-shader-widget
seankmartin Apr 9, 2024
5de6aa3
fix: transfer function correct interpolation
seankmartin Apr 9, 2024
a716b92
fix: dynamic transfer function size and correct GPU control
seankmartin Apr 9, 2024
ab43cc6
feat: remove range and size as TF params
seankmartin Apr 10, 2024
1936123
fix: parse shader directive for new TF
seankmartin Apr 10, 2024
16df32d
fix: JSON state parsing and saving for new TF
seankmartin Apr 11, 2024
3cece03
fix: new TF runs, but UI control is broken
seankmartin Apr 11, 2024
be77168
fix: remove accidental log
seankmartin Apr 11, 2024
7f47d8c
fix: control points display in correct position in UI
seankmartin Apr 11, 2024
6337593
fix: find control point near cursor in new TF
seankmartin Apr 11, 2024
4310bd9
fix: moving control points and setting color
seankmartin Apr 11, 2024
6790a63
fix: correct number of lines
seankmartin Apr 12, 2024
5d61c62
fix: display UI panel texture for TF
seankmartin Apr 12, 2024
f8e4f10
fix: render data from TF texture
seankmartin Apr 12, 2024
6d05449
fix: remove fixed size TF texture
seankmartin Apr 12, 2024
9c179ef
fix: link UI to JSON for control points
seankmartin Apr 12, 2024
5a4e218
fix: remove temps and TODOs
seankmartin Apr 12, 2024
2cd6829
fix: handle control point range for 0 and 1 points
seankmartin Apr 12, 2024
2cfe965
fix: test
seankmartin Apr 12, 2024
2ed5fdf
fix: unused code
seankmartin Apr 12, 2024
ffdf603
fix: can no longer lose control of point that you were trying to move
seankmartin Apr 12, 2024
7209fc0
fix: compute range after removing a point
seankmartin Apr 12, 2024
57450e7
refactor: clearer range update
seankmartin Apr 12, 2024
f5ef41b
Merge branch 'master' into feature/transfer-function-shader-widget
seankmartin Apr 18, 2024
0ee6746
fix: tf UI has correct texture indicator
seankmartin Apr 18, 2024
dbed2d4
feat: default intensity for transfer functions
seankmartin Apr 18, 2024
7065562
fix: don't crash on window[0] === window[1] in TF UI panel
seankmartin Apr 18, 2024
ee4f2df
fix: userIntensity always overwrites default
seankmartin Apr 18, 2024
1f81dcd
docs: update transfer function docs
seankmartin Apr 19, 2024
b2f8cc1
tests: fix a test for TFs with uint64 data
seankmartin Apr 19, 2024
7a50e55
feat: use non-interpolated value in TF for consistency and efficiency
seankmartin Apr 19, 2024
8f485db
fix: tests
seankmartin Apr 19, 2024
02b4153
Python(fix): fix transfer function control input
seankmartin Apr 19, 2024
c738b16
docs: fix formatting
seankmartin Apr 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ headlessly on Firefox using `xvfb-run`. On other platforms, tests can't be run

```shell
# For headless using Firefox on xvfb (Linux only)
sudo apt-get instrall xvfb # On Debian-based systems
tox -e firefox-xvfb # Run tests using non-headless Firefox
sudo apt-get install xvfb # On Debian-based systems
tox -e firefox-xvfb # Run tests using headless Firefox

# For non-headless using Chrome
tox -e chrome
Expand Down
1 change: 1 addition & 0 deletions python/neuroglancer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
LayerDataSource, # noqa: F401
LayerDataSources, # noqa: F401
InvlerpParameters, # noqa: F401
TransferFunctionParameters, # noqa: F401
ImageLayer, # noqa: F401
SkeletonRenderingOptions, # noqa: F401
StarredSegments, # noqa: F401
Expand Down
21 changes: 21 additions & 0 deletions python/neuroglancer/viewer_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,23 @@ class InvlerpParameters(JsonObjectWrapper):
channel = wrapped_property("channel", optional(typed_list(int)))


@export
class ControlPointsSpec(JsonObjectWrapper):
input = wrapped_property("input", optional(numbers.Number))
color = wrapped_property("color", optional(str))
opacity = wrapped_property("opacity", optional(float))


@export
class TransferFunctionParameters(JsonObjectWrapper):
range = wrapped_property("range", optional(array_wrapper(numbers.Number, 2)))
channel = wrapped_property("channel", optional(typed_list(int)))
controlPoints = wrapped_property(
"controlPoints", optional(typed_list(ControlPointsSpec))
)
color = wrapped_property("color", optional(str))


_UINT64_STR_PATTERN = re.compile("[0-9]+")


Expand All @@ -530,9 +547,13 @@ def _shader_control_parameters(v, _readonly=False):
if isinstance(v, numbers.Number):
return v
if isinstance(v, dict):
if "controlPoints" in v:
return TransferFunctionParameters(v, _readonly=_readonly)
return InvlerpParameters(v, _readonly=_readonly)
if isinstance(v, InvlerpParameters):
return v
if isinstance(v, TransferFunctionParameters):
return v
raise TypeError(f"Unexpected shader control parameters type: {type(v)}")


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def test_invlerp(webdriver):
"range": [0, 42],
},
},
opacity=1.0
)
s.layout = "xy"
s.cross_section_scale = 1e-6
Expand All @@ -62,6 +63,74 @@ def expect_color(color):
expect_color([0, 0, 0, 255])


def test_transfer_function(webdriver):
seankmartin marked this conversation as resolved.
Show resolved Hide resolved
shader = """
#uicontrol transferFunction colormap
void main() {
emitRGBA(colormap());
}
"""
shaderControls = {
"colormap": {
"controlPoints": [
{"input": 0, "color": "#000000", "opacity": 0.0},
{"input": 84, "color": "#ffffff", "opacity": 1.0},
],
"range": [0, 100],
"channel": [],
"color": "#ff00ff",
}
}
with webdriver.viewer.txn() as s:
s.dimensions = neuroglancer.CoordinateSpace(
names=["x", "y"], units="nm", scales=[1, 1]
)
s.position = [0.5, 0.5]
s.layers.append(
name="image",
layer=neuroglancer.ImageLayer(
source=neuroglancer.LocalVolume(
dimensions=s.dimensions,
data=np.full(shape=(1, 1), dtype=np.uint32, fill_value=42),
),
),
visible=True,
shader=shader,
shader_controls=shaderControls,
opacity=1.0,
blend="additive"
)
s.layout = "xy"
s.cross_section_scale = 1e-6
s.show_axis_lines = False
control = webdriver.viewer.state.layers["image"].shader_controls["colormap"]
assert isinstance(control, neuroglancer.TransferFunctionParameters)
np.testing.assert_equal(control.range, [0, 100])

def expect_color(color):
webdriver.sync()
screenshot = webdriver.viewer.screenshot(size=[10, 10]).screenshot
np.testing.assert_array_equal(
screenshot.image_pixels,
np.tile(np.array(color, dtype=np.uint8), (10, 10, 1)),
)

expect_color([64, 64, 64, 255])
with webdriver.viewer.txn() as s:
seankmartin marked this conversation as resolved.
Show resolved Hide resolved
s.layers["image"].shader_controls = {
"colormap": neuroglancer.TransferFunctionParameters(
controlPoints=[
{"input": 0, "color": "#000000", "opacity": 1.0},
{"input": 84, "color": "#ffffff", "opacity": 1.0},
],
range=[50, 90],
channel=[],
color="#ff00ff",
)
}
expect_color([0, 0, 0, 255])
seankmartin marked this conversation as resolved.
Show resolved Hide resolved


def test_slider(webdriver):
with webdriver.viewer.txn() as s:
s.dimensions = neuroglancer.CoordinateSpace(
Expand Down
28 changes: 28 additions & 0 deletions src/sliceview/image_layer_rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,34 @@ annotation layers). The one-parameter overload simply computes the inverse linea
the specified value within the range specified by the control. The zero-parameter overload returns
the inverse linear interpolation of the data value for configured channel/property.

### `transferFunction` controls

The `transferFunction` control type allows the user to specify a function which maps
each value in a numerical interval to an output color and opacity. The mapping function is defined by a series of control points. Each control point is a color and opacity value at a specific data input value. Any data point in the range that lies before the first control point is completely transparent. Any data point in the range that lies after the last control point has the value of the last control point. Any data point outside the range is clamped to lie within the range. In between control points, the color and opacity is linearly interpolated.
seankmartin marked this conversation as resolved.
Show resolved Hide resolved

Directive syntax:

```glsl
#uicontrol transferFunction <name>(range=[lower, higher], controlPoints=[[input, hexColorString, opacity]], channel=[], color="#rrggbb")
// For example:
#uicontrol transferFunction colormap(range=[0, 100], controlPoints=[[0.0, "#000000", 0.0], [100.0, "#ffffff", 1.0]], channel=[], color="#rrggbb")
```

The following parameters are supported:

- `range`: Optional. The default input range to map to an output. Must be specified as an array. May be overridden using the UI control. If not specified, defaults to the full range of
the data type for integer data types, and `[0, 1]` for float32. It is valid to specify an
inverted interval like `[50, 20]`.

- `controlPoints`: Optional. The points which define the input to output mapping. Must be specified as an array, with each value in the array of the form `[inputValue, hexStringColor, floatOpacity]`. The default transfer function is a simple knee from transparent black to fully opaque white.
seankmartin marked this conversation as resolved.
Show resolved Hide resolved

- `channel`: Optional. The channel to perform the mapping on.
If the rank of the channel coordinate space is 1, may be specified as a single number,
e.g. `channel=2`. Otherwise, must be specified as an array, e.g. `channel=[2, 3]`. May be
overriden using the UI control. If not specified, defaults to all-zero channel coordinates.

- `color`: Optional. The default color for new control points added via the UI control. Defaults to `#ffffff`, and must be specified as a hex string if provided `#rrggbb`.

## API

### Retrieving voxel channel value
Expand Down
1 change: 1 addition & 0 deletions src/sliceview/volume/renderlayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ void main() {

const endShader = () => {
if (shader === null) return;
shader.unbindTransferFunctionTextures();
if (prevChunkFormat !== null) {
prevChunkFormat!.endDrawing(gl, shader);
}
Expand Down
22 changes: 22 additions & 0 deletions src/util/array.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
spliceArray,
tile2dArray,
transposeArray2d,
findClosestMatchInSortedArray,
} from "#/util/array";

describe("partitionArray", () => {
Expand Down Expand Up @@ -205,3 +206,24 @@ describe("getMergeSplices", () => {
]);
});
});

describe("findClosestMatchInSortedArray", () => {
const compare = (a: number, b: number) => a - b;
it("works for empty array", () => {
expect(findClosestMatchInSortedArray([], 0, compare)).toEqual(-1);
});
it("works for simple examples", () => {
expect(findClosestMatchInSortedArray([0, 1, 2, 3], 0, compare)).toEqual(0);
expect(findClosestMatchInSortedArray([0, 1, 2, 3], 1, compare)).toEqual(1);
expect(findClosestMatchInSortedArray([0, 1, 2, 3], 2, compare)).toEqual(2);
expect(findClosestMatchInSortedArray([0, 1, 2, 3], 3, compare)).toEqual(3);
expect(findClosestMatchInSortedArray([0, 1, 2, 3], 4, compare)).toEqual(3);
expect(findClosestMatchInSortedArray([0, 1, 2, 3], -1, compare)).toEqual(0);
expect(findClosestMatchInSortedArray([0, 1, 2, 3], 1.5, compare)).toEqual(
1,
);
expect(findClosestMatchInSortedArray([0, 1, 2, 3], 1.6, compare)).toEqual(
2,
);
});
});
33 changes: 33 additions & 0 deletions src/util/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,39 @@ export function binarySearch<T>(
return ~low;
}

/**
* Returns the index of the element in `haystack` that is closest to `needle`, according to
* `compare`. If there are multiple elements that are equally close, the index of the first such
* element encountered is returned. If `haystack` is empty, returns -1.
*/
export function findClosestMatchInSortedArray<T>(
haystack: ArrayLike<T>,
needle: T,
compare: (a: T, b: T) => number,
low = 0,
high = haystack.length,
): number {
let bestIndex = -1;
let bestDistance = Infinity;
while (low < high) {
const mid = (low + high - 1) >> 1;
const compareResult = compare(needle, haystack[mid]);
if (compareResult > 0) {
low = mid + 1;
} else if (compareResult < 0) {
high = mid;
} else {
return mid;
}
const distance = Math.abs(compareResult);
if (distance < bestDistance) {
bestDistance = distance;
bestIndex = mid;
}
}
return bestIndex;
}

/**
* Returns the first index in `[begin, end)` for which `predicate` is `true`, or returns `end` if no
* such index exists.
Expand Down
1 change: 1 addition & 0 deletions src/volume_rendering/volume_render_layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ void main() {

const endShader = () => {
if (shader === null) return;
shader.unbindTransferFunctionTextures();
if (prevChunkFormat !== null) {
prevChunkFormat!.endDrawing(gl, shader);
}
Expand Down
36 changes: 36 additions & 0 deletions src/webgl/rectangle_grid_buffer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* @license
* Copyright 2023 Google Inc.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { createGriddedRectangleArray } from "#/webgl/rectangle_grid_buffer";

describe("createGriddedRectangleArray", () => {
it("creates a set of two squares for grid size=2 and rectangle width&height=2", () => {
const result = createGriddedRectangleArray(2, -1, 1, 1, -1);
expect(result).toEqual(
new Float32Array([
-1, 1, 0, 1, 0, -1, -1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 1, -1, 0, 1, 1,
-1, 0, -1,
]),
);
const resultReverse = createGriddedRectangleArray(2, 1, -1, -1, 1);
expect(resultReverse).toEqual(
new Float32Array([
1, -1, 0, -1, 0, 1, 1, -1, 0, 1, 1, 1, 0, -1, -1, -1, -1, 1, 0, -1, -1,
1, 0, 1,
]),
);
});
});
80 changes: 80 additions & 0 deletions src/webgl/rectangle_grid_buffer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* @license
* Copyright 2023 Google Inc.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { getMemoizedBuffer } from "#/webgl/buffer";
import { GL } from "#/webgl/context";
import { VERTICES_PER_QUAD } from "#/webgl/quad";

/**
* Create a Float32Array of vertices gridded in a rectangle
* Only grids along the x-axis are created, the y-axis is assumed to be the same for all grids
*/
export function createGriddedRectangleArray(
numGrids: number,
startX = -1,
endX = 1,
startY = 1,
endY = -1,
): Float32Array {
const result = new Float32Array(numGrids * VERTICES_PER_QUAD * 2);
const step = (endX - startX) / numGrids;
let currentx = startX;
for (let i = 0; i < numGrids; ++i) {
const index = i * VERTICES_PER_QUAD * 2;

// Triangle 1 - top-left, top-right, bottom-right
result[index] = currentx; // top-left x
result[index + 1] = startY; // top-left y
result[index + 2] = currentx + step; // top-right x
result[index + 3] = startY; // top-right y
result[index + 4] = currentx + step; // bottom-right x
result[index + 5] = endY; // bottom-right y

// Triangle 2 - top-left, bottom-right, bottom-left
result[index + 6] = currentx; // top-left x
result[index + 7] = startY; // top-left y
result[index + 8] = currentx + step; // bottom-right x
result[index + 9] = endY; // bottom-right y
result[index + 10] = currentx; // bottom-left x
result[index + 11] = endY; // bottom-left y
currentx += step;
}
return result;
}

/**
* Get a buffer of vertices gridded in a rectangle, useful for drawing grids, e.g. for a histogram
seankmartin marked this conversation as resolved.
Show resolved Hide resolved
* or a lookup table / heatmap
*/
export function getGriddedRectangleBuffer(
gl: GL,
numGrids: number,
startX = -1,
endX = 1,
startY = 1,
endY = -1,
) {
return getMemoizedBuffer(
gl,
WebGL2RenderingContext.ARRAY_BUFFER,
createGriddedRectangleArray,
numGrids,
startX,
endX,
startY,
endY,
).value;
}
Loading