-
-
Notifications
You must be signed in to change notification settings - Fork 382
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(ImageCroppingRegionsWidget): Add image cropping widget
- Loading branch information
Showing
5 changed files
with
912 additions
and
0 deletions.
There are no files selected for viewing
7 changes: 7 additions & 0 deletions
7
Sources/Interaction/Widgets/ImageCroppingRegionsRepresentation/Constants.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
const Orientation = { | ||
YZ: 0, | ||
XZ: 1, | ||
XY: 2, | ||
}; | ||
|
||
export default { Orientation }; |
278 changes: 278 additions & 0 deletions
278
Sources/Interaction/Widgets/ImageCroppingRegionsRepresentation/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,278 @@ | ||
import macro from 'vtk.js/Sources/macro'; | ||
import vtkActor from 'vtk.js/Sources/Rendering/Core/Actor'; | ||
import vtkCellPicker from 'vtk.js/Sources/Rendering/Core/CellPicker'; | ||
import vtkWidgetRepresentation from 'vtk.js/Sources/Interaction/Widgets/WidgetRepresentation'; | ||
import vtkMapper from 'vtk.js/Sources/Rendering/Core/Mapper'; | ||
import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData'; | ||
import Constants from 'vtk.js/Sources/Interaction/Widgets/ImageCroppingRegionsRepresentation/Constants'; | ||
|
||
const { vtkErrorMacro } = macro; | ||
const { Orientation } = Constants; | ||
|
||
// ---------------------------------------------------------------------------- | ||
// vtkImageCroppingRegionsRepresentation methods | ||
// ---------------------------------------------------------------------------- | ||
|
||
function vtkImageCroppingRegionsRepresentation(publicAPI, model) { | ||
// Set our className | ||
model.classHierarchy.push('vtkImageCroppingRegionsRepresentation'); | ||
|
||
// this is called at the end of the enclosing function. | ||
function constructor() { | ||
// set fields from parent classes | ||
model.placeFactor = 1; | ||
|
||
model.initialBounds = [0, 1, 0, 1, 0, 1]; | ||
// Format: | ||
// [xmin, xmax, ymin, ymax, zmin, zmax] | ||
model.planePositions = [0, 0, 0, 0, 0, 0]; | ||
model.sliceOrientation = Orientation.XY; | ||
model.slice = 0; | ||
|
||
// construct region polydata | ||
model.regionPolyData = vtkPolyData.newInstance(); | ||
model.regionPolyData.getPoints().setData(new Float32Array(16 * 3), 3); | ||
/* | ||
15-14-----11-10 | ||
| | | ||
12 13-----8 9 | ||
| | | | | ||
3 2------7 6 | ||
| | | ||
0--1------4--5 | ||
*/ | ||
// set polys | ||
model.regionPolyData.getPolys().setData(new Uint32Array([ | ||
4, 0, 1, 2, 3, | ||
4, 1, 4, 7, 2, | ||
4, 4, 5, 6, 7, | ||
4, 7, 6, 9, 8, | ||
4, 8, 9, 10, 11, | ||
4, 13, 8, 11, 14, | ||
4, 12, 13, 14, 15, | ||
4, 3, 2, 13, 12, | ||
]), 1); | ||
|
||
model.mapper = vtkMapper.newInstance(); | ||
model.actor = vtkActor.newInstance(); | ||
|
||
model.mapper.setInputData(model.regionPolyData); | ||
model.actor.setMapper(model.mapper); | ||
model.actor.getProperty().setEdgeColor(1.0, 0, 0); | ||
model.actor.getProperty().setEdgeVisibility(true); | ||
publicAPI.setOpacity(model.opacity); | ||
|
||
// set picker | ||
model.cursorPicker = vtkCellPicker.newInstance(); | ||
} | ||
|
||
publicAPI.getActors = () => model.actor; | ||
publicAPI.getNestedProps = () => publicAPI.getActors(); | ||
|
||
// Reorders a bounds array such that each (a,b) pairing is a | ||
// (min,max) pairing. | ||
function reorderBounds(bounds) { | ||
for (let i = 0; i < 6; i += 2) { | ||
if (bounds[i] > bounds[i + 1]) { | ||
const tmp = bounds[i + 1]; | ||
bounds[i + 1] = bounds[i]; | ||
bounds[i] = tmp; | ||
} | ||
} | ||
} | ||
|
||
publicAPI.placeWidget = (...bounds) => { | ||
const boundsArray = []; | ||
|
||
for (let i = 0; i < bounds.length; i++) { | ||
boundsArray.push(bounds[i]); | ||
} | ||
|
||
if (boundsArray.length !== 6) { | ||
return; | ||
} | ||
|
||
// make sure each bounds pairing is monotonic | ||
reorderBounds(boundsArray); | ||
|
||
const newBounds = []; | ||
const center = []; | ||
publicAPI.adjustBounds(boundsArray, newBounds, center); | ||
|
||
for (let i = 0; i < 6; i++) { | ||
model.initialBounds[i] = newBounds[i]; | ||
} | ||
|
||
model.initialLength = Math.sqrt( | ||
((newBounds[1] - newBounds[0]) * (newBounds[1] - newBounds[0])) + | ||
((newBounds[3] - newBounds[2]) * (newBounds[3] - newBounds[2])) + | ||
((newBounds[5] - newBounds[4]) * (newBounds[5] - newBounds[4]))); | ||
|
||
// set plane positions | ||
publicAPI.setPlanePositions(...model.initialBounds); | ||
}; | ||
|
||
publicAPI.setSlice = (slice, sliceOrientation) => { | ||
model.slice = slice; | ||
model.sliceOrientation = sliceOrientation; | ||
publicAPI.updateGeometry(); | ||
}; | ||
|
||
publicAPI.setOpacity = (opacityValue) => { | ||
const opacity = Math.max(0, Math.min(1, opacityValue)); | ||
model.actor.getProperty().setOpacity(opacity); | ||
}; | ||
|
||
publicAPI.setPlanePositions = (...positions) => { | ||
if (positions.length !== 6) { | ||
vtkErrorMacro('setPlanePositions() must be given a 3D boundaries array'); | ||
return; | ||
} | ||
|
||
// swap around plane boundaries if not in order | ||
reorderBounds(positions); | ||
|
||
// make sure each position is within the bounds | ||
for (let i = 0; i < 6; i += 2) { | ||
if (positions[i] < model.initialBounds[i] || positions[i] > model.initialBounds[i + 1]) { | ||
positions[i] = model.initialBounds[i]; | ||
} | ||
if (positions[i + 1] < model.initialBounds[i] || positions[i + 1] > model.initialBounds[i + 1]) { | ||
positions[i + 1] = model.initialBounds[i + 1]; | ||
} | ||
} | ||
|
||
// set plane positions | ||
model.planePositions = positions; | ||
publicAPI.updateGeometry(); | ||
}; | ||
|
||
publicAPI.updateGeometry = () => { | ||
const slicePos = model.slice; | ||
const verts = model.regionPolyData.getPoints().getData(); | ||
const [xBMin, xBMax, yBMin, yBMax, zBMin, zBMax] = model.initialBounds; | ||
const [xPLower, xPUpper, yPLower, yPUpper, zPLower, zPUpper] = model.planePositions; | ||
|
||
// set vert coordinates depending on slice orientation | ||
switch (model.sliceOrientation) { | ||
case Orientation.YZ: | ||
verts.set([slicePos, yBMin, zBMin], 0); | ||
verts.set([slicePos, yPLower, zBMin], 3); | ||
verts.set([slicePos, yPLower, zPLower], 6); | ||
verts.set([slicePos, yBMin, zPLower], 9); | ||
verts.set([slicePos, yPUpper, zBMin], 12); | ||
verts.set([slicePos, yBMax, zBMin], 15); | ||
verts.set([slicePos, yBMax, zPLower], 18); | ||
verts.set([slicePos, yPUpper, zPLower], 21); | ||
verts.set([slicePos, yPUpper, zPUpper], 24); | ||
verts.set([slicePos, yBMax, zPUpper], 27); | ||
verts.set([slicePos, yBMax, zBMax], 30); | ||
verts.set([slicePos, yPUpper, zBMax], 33); | ||
verts.set([slicePos, yBMin, zPUpper], 36); | ||
verts.set([slicePos, yPLower, zPUpper], 39); | ||
verts.set([slicePos, yPLower, zBMax], 42); | ||
verts.set([slicePos, yBMin, zBMax], 45); | ||
break; | ||
case Orientation.XZ: | ||
verts.set([xBMin, slicePos, zBMin], 0); | ||
verts.set([xPLower, slicePos, zBMin], 3); | ||
verts.set([xPLower, slicePos, zPLower], 6); | ||
verts.set([xBMin, slicePos, zPLower], 9); | ||
verts.set([xPUpper, slicePos, zBMin], 12); | ||
verts.set([xBMax, slicePos, zBMin], 15); | ||
verts.set([xBMax, slicePos, zPLower], 18); | ||
verts.set([xPUpper, slicePos, zPLower], 21); | ||
verts.set([xPUpper, slicePos, zPUpper], 24); | ||
verts.set([xBMax, slicePos, zPUpper], 27); | ||
verts.set([xBMax, slicePos, zBMax], 30); | ||
verts.set([xPUpper, slicePos, zBMax], 33); | ||
verts.set([xBMin, slicePos, zPUpper], 36); | ||
verts.set([xPLower, slicePos, zPUpper], 39); | ||
verts.set([xPLower, slicePos, zBMax], 42); | ||
verts.set([xBMin, slicePos, zBMax], 45); | ||
break; | ||
case Orientation.XY: | ||
verts.set([xBMin, yBMin, slicePos], 0); | ||
verts.set([xPLower, yBMin, slicePos], 3); | ||
verts.set([xPLower, yPLower, slicePos], 6); | ||
verts.set([xBMin, yPLower, slicePos], 9); | ||
verts.set([xPUpper, yBMin, slicePos], 12); | ||
verts.set([xBMax, yBMin, slicePos], 15); | ||
verts.set([xBMax, yPLower, slicePos], 18); | ||
verts.set([xPUpper, yPLower, slicePos], 21); | ||
verts.set([xPUpper, yPUpper, slicePos], 24); | ||
verts.set([xBMax, yPUpper, slicePos], 27); | ||
verts.set([xBMax, yBMax, slicePos], 30); | ||
verts.set([xPUpper, yBMax, slicePos], 33); | ||
verts.set([xBMin, yPUpper, slicePos], 36); | ||
verts.set([xPLower, yPUpper, slicePos], 39); | ||
verts.set([xPLower, yBMax, slicePos], 42); | ||
verts.set([xBMin, yBMax, slicePos], 45); | ||
break; | ||
default: | ||
// noop | ||
} | ||
model.regionPolyData.modified(); | ||
}; | ||
|
||
publicAPI.getBounds = () => model.initialBounds; | ||
|
||
publicAPI.buildRepresentation = () => { | ||
if (model.renderer) { | ||
if (!model.placed) { | ||
model.validPick = 1; | ||
model.placed = 1; | ||
} | ||
publicAPI.modified(); | ||
} | ||
}; | ||
|
||
publicAPI.setProperty = (property) => { | ||
model.actor.setProperty(property); | ||
}; | ||
|
||
// invoke the constructor after setting up public methods | ||
constructor(); | ||
} | ||
|
||
// ---------------------------------------------------------------------------- | ||
// Object factory | ||
// ---------------------------------------------------------------------------- | ||
|
||
const DEFAULT_VALUES = { | ||
constraintAxis: -1, | ||
translationMode: 1, | ||
waitingForMotion: 0, | ||
hotSpotSize: 0.05, | ||
opacity: 0.5, | ||
}; | ||
|
||
// ---------------------------------------------------------------------------- | ||
|
||
export function extend(publicAPI, model, initialValues = {}) { | ||
Object.assign(model, DEFAULT_VALUES, initialValues); | ||
|
||
// Inheritance | ||
vtkWidgetRepresentation.extend(publicAPI, model, initialValues); | ||
|
||
macro.setGet(publicAPI, model, ['translationMode']); | ||
|
||
macro.get(publicAPI, model, [ | ||
'initialBounds', | ||
'planePositions', | ||
'sliceOrientation', | ||
'slice', | ||
'opacity', | ||
]); | ||
|
||
// Object methods | ||
vtkImageCroppingRegionsRepresentation(publicAPI, model); | ||
} | ||
|
||
// ---------------------------------------------------------------------------- | ||
|
||
export const newInstance = macro.newInstance(extend, 'vtkImageCroppingRegionsRepresentation'); | ||
|
||
// ---------------------------------------------------------------------------- | ||
|
||
export default { newInstance, extend }; |
24 changes: 24 additions & 0 deletions
24
Sources/Interaction/Widgets/ImageCroppingRegionsWidget/Constants.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
const WidgetState = { | ||
IDLE: 0, | ||
MOVE_XLOWER: -1, | ||
MOVE_XUPPER: -2, | ||
MOVE_YLOWER: -3, | ||
MOVE_YUPPER: -4, | ||
MOVE_XLOWER_YLOWER: -5, | ||
MOVE_XLOWER_YUPPER: -6, | ||
MOVE_XUPPER_YLOWER: -7, | ||
MOVE_XUPPER_YUPPER: -8, | ||
|
||
MOVE_LEFT: 1, | ||
MOVE_RIGHT: 2, | ||
MOVE_BOTTOM: 3, | ||
MOVE_TOP: 4, | ||
MOVE_LEFT_BOTTOM: 5, | ||
MOVE_LEFT_TOP: 6, | ||
MOVE_RIGHT_BOTTOM: 7, | ||
MOVE_RIGHT_TOP: 8, | ||
}; | ||
|
||
const SliceNormals = ['X', 'Y', 'Z']; | ||
|
||
export default { WidgetState, SliceNormals }; |
83 changes: 83 additions & 0 deletions
83
Sources/Interaction/Widgets/ImageCroppingRegionsWidget/example/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import 'vtk.js/Sources/favicon'; | ||
|
||
import vtkFullScreenRenderWindow from 'vtk.js/Sources/Rendering/Misc/FullScreenRenderWindow'; | ||
import vtkHttpDataSetReader from 'vtk.js/Sources/IO/Core/HttpDataSetReader'; | ||
import vtkImageMapper from 'vtk.js/Sources/Rendering/Core/ImageMapper'; | ||
import vtkImageSlice from 'vtk.js/Sources/Rendering/Core/ImageSlice'; | ||
import vtkInteractorStyleImage from 'vtk.js/Sources/Interaction/Style/InteractorStyleImage'; | ||
import vtkImageCroppingRegionsWidget from 'vtk.js/Sources/Interaction/Widgets/ImageCroppingRegionsWidget'; | ||
|
||
// ---------------------------------------------------------------------------- | ||
// Standard rendering code setup | ||
// ---------------------------------------------------------------------------- | ||
|
||
const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance(); | ||
const renderer = fullScreenRenderer.getRenderer(); | ||
const renderWindow = fullScreenRenderer.getRenderWindow(); | ||
const interactorStyle2D = vtkInteractorStyleImage.newInstance(); | ||
console.log(interactorStyle2D); | ||
renderWindow.getInteractor().setInteractorStyle(interactorStyle2D); | ||
renderer.getActiveCamera().setParallelProjection(true); | ||
|
||
// set the current image number to the first image | ||
interactorStyle2D.setCurrentImageNumber(0); | ||
|
||
// ---------------------------------------------------------------------------- | ||
// Create widget | ||
// ---------------------------------------------------------------------------- | ||
const widget = vtkImageCroppingRegionsWidget.newInstance(); | ||
widget.setInteractor(renderWindow.getInteractor()); | ||
|
||
// called when the volume is loaded | ||
function setupWidget(imageMapper) { | ||
widget.setImageMapper(imageMapper); | ||
widget.setEnable(true); | ||
|
||
renderWindow.render(); | ||
} | ||
|
||
// ---------------------------------------------------------------------------- | ||
// Set up volume | ||
// ---------------------------------------------------------------------------- | ||
const mapper = vtkImageMapper.newInstance(); | ||
const actor = vtkImageSlice.newInstance(); | ||
actor.setMapper(mapper); | ||
renderer.addViewProp(actor); | ||
|
||
const reader = vtkHttpDataSetReader.newInstance({ fetchGzip: true }); | ||
reader.setUrl(`${__BASE_PATH__}/data/volume/headsq.vti`, { loadData: true }).then(() => { | ||
const data = reader.getOutputData(); | ||
// we don't care about image direction here | ||
data.setDirection(1, 0, 0, 0, 1, 0, 0, 0, 1); | ||
|
||
mapper.setInputData(data); | ||
// change me! | ||
const sliceMode = 0; | ||
const sliceNormal = ['X', 'Y', 'Z'][sliceMode]; | ||
mapper.setCurrentSlicingMode(sliceMode); | ||
mapper[`set${sliceNormal}Slice`](32); | ||
|
||
const camPosition = renderer.getActiveCamera().getFocalPoint().map((v, idx) => (idx === sliceMode ? (v + 1) : v)); | ||
const viewUp = [0, 0, 0]; | ||
// viewUp[(sliceMode + 2) % 3] = 1; | ||
viewUp[2] = 1; | ||
renderer.getActiveCamera().set({ position: camPosition, viewUp }); | ||
console.log(renderer.getActiveCamera().getViewUp()); | ||
|
||
// create our cropping widget | ||
setupWidget(mapper); | ||
|
||
renderer.resetCamera(); | ||
renderWindow.render(); | ||
}); | ||
|
||
renderWindow.render(); | ||
|
||
// ----------------------------------------------------------- | ||
// Make some variables global so that you can inspect and | ||
// modify objects in your browser's developer console: | ||
// ----------------------------------------------------------- | ||
|
||
global.renderer = renderer; | ||
global.renderWindow = renderWindow; | ||
global.widget = widget; |
Oops, something went wrong.