Skip to content

Commit

Permalink
Merge pull request #728 from PaulHax/color-range-editor
Browse files Browse the repository at this point in the history
feat: add independent GUI change of color range
  • Loading branch information
thewtex authored Dec 19, 2023
2 parents 2750710 + 12ae49f commit 50332ae
Show file tree
Hide file tree
Showing 20 changed files with 1,045 additions and 898 deletions.
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"itk-image-io": "^1.0.0-b.84",
"itk-mesh-io": "^1.0.0-b.84",
"itk-viewer-color-maps": "^1.2.0",
"itk-viewer-transfer-function-editor": "^1.2.5",
"itk-viewer-transfer-function-editor": "^1.3.0",
"itk-wasm": "^1.0.0-b.83",
"mobx": "^5.15.7",
"mousetrap": "^1.6.5",
Expand Down
18 changes: 8 additions & 10 deletions src/IO/Analyze/computeHistograms.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const updateHistogramWorkerPool = webWorkerPromiseWorkerPool(
'updateHistogram'
)

const BIN_COUNT_DEFAULT = 256

export const computeHistogram = async (
values,
component,
Expand All @@ -20,16 +22,12 @@ export const computeHistogram = async (
) => {
const numberOfSplits = numberOfWorkers

let numberOfBins = 256
if (
typeof values !== typeof Float32Array ||
typeof values !== typeof Float64Array
) {
const intBins = max - min + 1
if (intBins < numberOfBins) {
numberOfBins = intBins
}
}
const isFloatValues =
values instanceof Float32Array || values instanceof Float64Array
const numberOfBins = isFloatValues
? BIN_COUNT_DEFAULT
: // only need a bin for each possible integer value
Math.min(max - min + 1, BIN_COUNT_DEFAULT)

const taskArgs = new Array(numberOfSplits)
if (haveSharedArrayBuffer && values.buffer instanceof SharedArrayBuffer) {
Expand Down
3 changes: 3 additions & 0 deletions src/Rendering/Images/createImageRenderingActor.js
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,9 @@ const eventResponses = {
IMAGE_COLOR_RANGE_CHANGED: {
actions: [assignColorRange, 'applyColorRange'],
},
IMAGE_COLOR_RANGE_POINTS_CHANGED: {
actions: 'mapToColorFunctionRange',
},
IMAGE_COLOR_RANGE_BOUNDS_CHANGED: {
actions: ['applyColorRangeBounds'],
},
Expand Down
5 changes: 5 additions & 0 deletions src/Rendering/Images/createImagesRenderingMachine.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ function createImagesRenderingMachine(options, context) {
to: (c, e) => `imageRenderingActor-${e.data.name}`,
}),
},
IMAGE_COLOR_RANGE_POINTS_CHANGED: {
actions: send((_, e) => e, {
to: (c, e) => `imageRenderingActor-${e.data.name}`,
}),
},
IMAGE_COLOR_MAP_CHANGED: {
actions: send((_, e) => e, {
to: (c, e) => `imageRenderingActor-${e.data.name}`,
Expand Down
51 changes: 8 additions & 43 deletions src/Rendering/VTKJS/Images/applyColorRange.js
Original file line number Diff line number Diff line change
@@ -1,52 +1,17 @@
function applyColorRange(context, { data: { name, component, range } }) {
const actorContext = context.images.actorContext.get(name)

function applyColorRange(context, e) {
const {
data: { component, range },
} = e
if (!context.images.colorTransferFunctions) {
return
}
const colorTransferFunction = context.images.colorTransferFunctions.get(
component
)
colorTransferFunction.setMappingRange(range[0], range[1])
colorTransferFunction.updateRange()

if (actorContext.piecewiseFunctionPointsAutoAdjust.get(component)) {
// Rescale opacity points to fit range
let fullRange = range
if (actorContext.colorRangeBounds.has(component)) {
fullRange = actorContext.colorRangeBounds.get(component)
}
const diff = fullRange[1] - fullRange[0]
const colorRangeNormalized = [
(range[0] - fullRange[0]) / diff,
(range[1] - fullRange[0]) / diff,
]
const normDelta = colorRangeNormalized[1] - colorRangeNormalized[0]

const oldPoints = actorContext.piecewiseFunctionPoints.get(component)
const xValues = oldPoints.map(([x]) => x)
// if 1 point, assume whole range
const maxOldPoints = xValues.length > 1 ? Math.max(...xValues) : 1
let minOldPoints = xValues.length > 1 ? Math.min(...xValues) : 0
let rangeOldPoints = maxOldPoints - minOldPoints
if (rangeOldPoints === 0) {
minOldPoints = 0
rangeOldPoints = 1
}
const points = oldPoints
// find normalized position of old points
.map(([x, y]) => [(x - minOldPoints) / rangeOldPoints, y])
// rescale to new range
.map(([x, y]) => {
return [x * normDelta + colorRangeNormalized[0], y]
})

context.service.send({
type: 'IMAGE_PIECEWISE_FUNCTION_POINTS_CHANGED',
data: {
name,
component,
points,
},
})
}
context.service.send('RENDER')
}

export default applyColorRange
32 changes: 0 additions & 32 deletions src/Rendering/VTKJS/Images/applyColorRangeBounds.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,37 +18,5 @@ const assignColorRangeBounds = (
}

export const applyColorRangeBounds = (context, event) => {
const {
data: { name, component, range: newRange },
} = event
const { colorRangeBounds } = context.images.actorContext.get(name)
const oldBounds = colorRangeBounds.get(component)

assignColorRangeBounds(context, event)

if (!oldBounds) return

// Rescale opacity points to fit range

const oldRangeDiff = oldBounds[1] - oldBounds[0]
const newRangeDiff = newRange[1] - newRange[0]

const actorContext = context.images.actorContext.get(name)
const oldPoints = actorContext.piecewiseFunctionPoints.get(component)
const points = oldPoints
// find real intensity value of normalized points
.map(([x, y]) => [x * oldRangeDiff + oldBounds[0], y])
// rescale to new range
.map(([x, y]) => {
return [(x - newRange[0]) / newRangeDiff, y]
})

context.service.send({
type: 'IMAGE_PIECEWISE_FUNCTION_POINTS_CHANGED',
data: {
name,
component,
points,
},
})
}
8 changes: 1 addition & 7 deletions src/Rendering/VTKJS/Images/applyPiecewiseFunction.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
function applyPiecewiseFunction(context, event) {
const name = event.data.name
const component = event.data.component
const range = event.data.range
// const range = event.data.range
const nodes = event.data.nodes

const actorContext = context.images.actorContext.get(name)
Expand All @@ -17,12 +17,6 @@ function applyPiecewiseFunction(context, event) {
const sliceNodes = nodes.length > 2 ? nodes.slice(1, -1) : nodes // if more than 2, remove "window" nodes with y = 0
slicePiecewiseFunction.setNodes(sliceNodes)

const colorTransferFunction = context.images.colorTransferFunctions.get(
component
)
colorTransferFunction.setMappingRange(...range)
colorTransferFunction.updateRange()

context.service.send('RENDER')
}
}
Expand Down
65 changes: 39 additions & 26 deletions src/Rendering/VTKJS/Images/applyRenderedImage.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,35 @@ function applyRenderedImage(context, { data: { name } }) {
? getDefaultRangeByDataType(dataArray.getDataType())
: [dataMin, dataMax]

const storedColorRange = colorRanges.get(componentIndex)
if (colorRangesAutoAdjust.get(componentIndex) || !storedColorRange) {
const fullRange = actorContext.colorRangeBounds.get(componentIndex) ?? [
0,
1,
]
const range = storedColorRange ?? [0.2, 0.8]
// rescale to new range
const diff = fullRange[1] - fullRange[0]
const colorRangeNormalized = [
(range[0] - fullRange[0]) / diff,
(range[1] - fullRange[0]) / diff,
]
const newDelta = newMax - newMin
const newRange = colorRangeNormalized.map(x => {
return x * newDelta + newMin
})

context.service.send({
type: 'IMAGE_COLOR_RANGE_CHANGED',
data: {
name,
component: componentIndex,
range: newRange,
keepAutoAdjusting: true,
},
})
}

if (colorRangeBoundsAutoAdjust.get(componentIndex)) {
const oldRange = colorRangeBounds.get(componentIndex) ?? [
Number.POSITIVE_INFINITY,
Expand All @@ -304,32 +333,6 @@ function applyRenderedImage(context, { data: { name } }) {
})
}
}

const storedColorRange = colorRanges.get(componentIndex)
const oldRange = storedColorRange ?? [
Number.POSITIVE_INFINITY,
Number.NEGATIVE_INFINITY,
]
if (colorRangesAutoAdjust.get(componentIndex) || !storedColorRange) {
// only grow range
const newRange = [
Math.min(newMin, oldRange[0]),
Math.max(newMax, oldRange[1]),
]
const hasChanged = newRange.some((value, i) => value !== oldRange[i])

if (hasChanged) {
context.service.send({
type: 'IMAGE_COLOR_RANGE_CHANGED',
data: {
name,
component: componentIndex,
range: newRange,
keepAutoAdjusting: true,
},
})
}
}
})

if (labelImage) {
Expand Down Expand Up @@ -389,6 +392,16 @@ function applyRenderedImage(context, { data: { name } }) {
type: 'IMAGE_COLOR_MAP_CHANGED',
data: { name, component, colorMap },
})

// Update piecewise function nodes after we have data range
// Without this piecewise functions are never "applied"
const points = context.images.actorContext
.get(name)
.piecewiseFunctionPoints.get(component)
context.service.send({
type: 'IMAGE_PIECEWISE_FUNCTION_POINTS_CHANGED',
data: { name, component, points },
})
}

// call after representations are created
Expand Down
3 changes: 3 additions & 0 deletions src/Rendering/VTKJS/Images/imagesRenderingMachineOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import applyLabelNames from './applyLabelNames'
import applyLabelImageWeights from './applyLabelImageWeights'
import applySelectedLabel from './applySelectedLabel'
import mapToPiecewiseFunctionNodes from './mapToPiecewiseFunctionNodes'
import mapToColorFunctionRange from './mapToColorFunctionRange'
import { getBoundsOfFullImage } from '../Main/croppingPlanes'
import { computeRenderedBounds } from '../Main/computeRenderedBounds'
import { applyCinematicChanged } from './applyCinematicChanged'
Expand Down Expand Up @@ -105,6 +106,7 @@ const imagesRenderingMachineOptions = {
applyColorRangeBounds,
applyColorMap,
mapToPiecewiseFunctionNodes,
mapToColorFunctionRange,

toggleInterpolation,
applyShadow,
Expand Down Expand Up @@ -143,6 +145,7 @@ const imagesRenderingMachineOptions = {
const fusedImage = actorContext.fusedImage
if (!fusedImage) {
console.warn('No image to download')
return
}
const itkImage = copyImage(convertVtkToItkImage(fusedImage, true))
writeImageArrayBuffer(null, itkImage, fileName).then(
Expand Down
32 changes: 32 additions & 0 deletions src/Rendering/VTKJS/Images/mapToColorFunctionRange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
function mapToColorFunctionRange(
context,
{ data: { name, component, points } }
) {
const actorContext = context.images.actorContext.get(name)
const dataRange = actorContext.colorRangeBounds.get(component)

if (!dataRange) return // viewer.setImagePiecewiseFunctionPoints called at start

const rangeDelta = dataRange[1] - dataRange[0]
const range = points.map(v => v * rangeDelta + dataRange[0])

// compare with current values to see if updated needed
const { colorRanges } = context.images.actorContext.get(name)
if (colorRanges.has(component)) {
const currentRange = colorRanges.get(component)
if (currentRange[0] === range[0] && currentRange[1] === range[1]) {
return
}
}

context.service.send({
type: 'IMAGE_COLOR_RANGE_CHANGED',
data: {
name,
component,
range,
},
})
}

export default mapToColorFunctionRange
3 changes: 3 additions & 0 deletions src/Rendering/createRenderingMachine.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ const createRenderingMachine = (options, context) => {
IMAGE_COLOR_RANGE_CHANGED: {
actions: forwardTo('images'),
},
IMAGE_COLOR_RANGE_POINTS_CHANGED: {
actions: forwardTo('images'),
},
IMAGE_COLOR_MAP_CHANGED: {
actions: forwardTo('images'),
},
Expand Down
5 changes: 3 additions & 2 deletions src/UI/Layers/createLayersUIMachine.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,20 +201,21 @@ const assignImageContext = assign({
actorContext.colorMaps.set(component, colorMap)
}

// Assign default piecewiseFunction
for (let component = 0; component < components; component++) {
if (!actorContext.piecewiseFunctionPoints.has(component)) {
// Assign default piecewiseFunction
const points = context.use2D
? [
[0, 1],
[1, 1],
]
: [
[0, 0],
[1, 1],
[0.9, 0.9],
]
actorContext.piecewiseFunctionPoints.set(component, points)
}
actorContext.colorRanges.set(component, [0.2, 0.8])
}

actorContext.colorRangesAutoAdjust = new Map(
Expand Down
Loading

0 comments on commit 50332ae

Please sign in to comment.