From 55368dc5479281ecfad3a4b97d89dd67777cee9f Mon Sep 17 00:00:00 2001 From: Will Schroeder Date: Tue, 21 Nov 2017 15:56:05 -0500 Subject: [PATCH] feat(ImageMarchingCubes): Added 2D image or slice contouring A new isocontouring algorithm for 2D images or a zslice from a volume has been added. Multiple contours are supported, and merging intersection points along edges may be enabled or disabled. --- .../Common/DataModel/ImplicitBoolean/index.js | 12 + .../General/ImageMarchingSquares/api.md | 24 ++ .../General/ImageMarchingSquares/caseTable.js | 48 ++++ .../example/controller.html | 20 ++ .../ImageMarchingSquares/example/index.js | 110 +++++++++ .../General/ImageMarchingSquares/index.js | 211 ++++++++++++++++++ Sources/Filters/General/index.js | 2 + 7 files changed, 427 insertions(+) create mode 100644 Sources/Filters/General/ImageMarchingSquares/api.md create mode 100644 Sources/Filters/General/ImageMarchingSquares/caseTable.js create mode 100644 Sources/Filters/General/ImageMarchingSquares/example/controller.html create mode 100644 Sources/Filters/General/ImageMarchingSquares/example/index.js create mode 100644 Sources/Filters/General/ImageMarchingSquares/index.js diff --git a/Sources/Common/DataModel/ImplicitBoolean/index.js b/Sources/Common/DataModel/ImplicitBoolean/index.js index cac10673a1a..3135ac56e19 100644 --- a/Sources/Common/DataModel/ImplicitBoolean/index.js +++ b/Sources/Common/DataModel/ImplicitBoolean/index.js @@ -19,6 +19,18 @@ function vtkImplicitBoolean(publicAPI, model) { // Set our className model.classHierarchy.push('vtkImplicitBoolean'); + publicAPI.getMTime = () => { + let mTime = model.superClass.getMTime(); + if (!model.functions || model.functions.length <= 0) { + return mTime; + } + + for (let i = 0; i < model.functions.length; ++i) { + mTime = Math.max(mTime, model.functions[i].getMTime()); + } + return mTime; + }; + publicAPI.getOperationAsString = () => macro.enumToString(Operation, model.operation); publicAPI.setOperationToUnion = () => publicAPI.setOperation(0); diff --git a/Sources/Filters/General/ImageMarchingSquares/api.md b/Sources/Filters/General/ImageMarchingSquares/api.md new file mode 100644 index 00000000000..ff005c52648 --- /dev/null +++ b/Sources/Filters/General/ImageMarchingSquares/api.md @@ -0,0 +1,24 @@ +## Introduction + +vtkImageMarchingSquares - isocontour an image (or slice from a volume) + +Given a specified contour value, generate isolines using the +Marching Squares algorithm (the 2D version of the 3D Marching Cubes +algorithm). + +## Public API + +### contourValues + +Set/Get an array of isocontour values. + +### sliceNumber + +Set/Get the k-slice number of the input volume. By default the +sliceNumber = 0. + +### mergePoints + +As lines forming the isolines are generated, indicate whether +conincident points are to be merged. Merging produces connected polylines +at the cost of additional memory and computation. diff --git a/Sources/Filters/General/ImageMarchingSquares/caseTable.js b/Sources/Filters/General/ImageMarchingSquares/caseTable.js new file mode 100644 index 00000000000..f39efc95416 --- /dev/null +++ b/Sources/Filters/General/ImageMarchingSquares/caseTable.js @@ -0,0 +1,48 @@ +// ---------------------------------------------------------------------------- +// Marching squares case functions (using lines to generate the 2D tessellation). +// For each case, a list of edge ids that form the triangles. A -1 marks the +// end of the list of edges. Edges are taken three at a time to generate +// triangle points. +// ---------------------------------------------------------------------------- +const MARCHING_SQUARES_CASES = [ + [-1, -1, -1, -1, -1], /* 0 */ + [0, 3, -1, -1, -1], /* 1 */ + [1, 0, -1, -1, -1], /* 2 */ + [1, 3, -1, -1, -1], /* 3 */ + [2, 1, -1, -1, -1], /* 4 */ + [0, 3, 2, 1, -1], /* 5 */ + [2, 0, -1, -1, -1], /* 6 */ + [2, 3, -1, -1, -1], /* 7 */ + [3, 2, -1, -1, -1], /* 8 */ + [0, 2, -1, -1, -1], /* 9 */ + [1, 0, 3, 2, -1], /* 10 */ + [1, 2, -1, -1, -1], /* 11 */ + [3, 1, -1, -1, -1], /* 12 */ + [0, 1, -1, -1, -1], /* 13 */ + [3, 0, -1, -1, -1], /* 14 */ + [-1, -1, -1, -1, -1], /* 15 */ +]; + +const EDGES = [ + [0, 1], + [1, 3], + [2, 3], + [0, 2], +]; + +function getCase(index) { + return MARCHING_SQUARES_CASES[index]; +} + +// Define the four edges of the pixel by the following pairs of vertices +function getEdge(eid) { + return EDGES[eid]; +} + +// ---------------------------------------------------------------------------- +// Static API +// ---------------------------------------------------------------------------- +export default { + getCase, + getEdge, +}; diff --git a/Sources/Filters/General/ImageMarchingSquares/example/controller.html b/Sources/Filters/General/ImageMarchingSquares/example/controller.html new file mode 100644 index 00000000000..cb3ff709eea --- /dev/null +++ b/Sources/Filters/General/ImageMarchingSquares/example/controller.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + +
Volume resolution + +
Radius + +
Merge Points + +
diff --git a/Sources/Filters/General/ImageMarchingSquares/example/index.js b/Sources/Filters/General/ImageMarchingSquares/example/index.js new file mode 100644 index 00000000000..f5ba68b074d --- /dev/null +++ b/Sources/Filters/General/ImageMarchingSquares/example/index.js @@ -0,0 +1,110 @@ +import 'vtk.js/Sources/favicon'; + +import vtkActor from 'vtk.js/Sources/Rendering/Core/Actor'; +import vtkFullScreenRenderWindow from 'vtk.js/Sources/Rendering/Misc/FullScreenRenderWindow'; +import vtkImageMarchingSquares from 'vtk.js/Sources/Filters/General/ImageMarchingSquares'; +import vtkOutlineFilter from 'vtk.js/Sources/Filters/General/OutlineFilter'; +import vtkMapper from 'vtk.js/Sources/Rendering/Core/Mapper'; +import vtkSampleFunction from 'vtk.js/Sources/Imaging/Hybrid/SampleFunction'; +import vtkSphere from 'vtk.js/Sources/Common/DataModel/Sphere'; +// import vtkPlane from 'vtk.js/Sources/Common/DataModel/Plane'; +import vtkImplicitBoolean from 'vtk.js/Sources/Common/DataModel/ImplicitBoolean'; + +import controlPanel from './controller.html'; + +const { Operation } = vtkImplicitBoolean; + +// ---------------------------------------------------------------------------- +// Standard rendering code setup +// ---------------------------------------------------------------------------- + +const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance(); +const renderer = fullScreenRenderer.getRenderer(); +const renderWindow = fullScreenRenderer.getRenderWindow(); + +// ---------------------------------------------------------------------------- +// Example code +// ---------------------------------------------------------------------------- +const actor = vtkActor.newInstance(); +renderer.addActor(actor); + +const mapper = vtkMapper.newInstance(); +actor.setMapper(mapper); + +// Build pipeline +const sphere = vtkSphere.newInstance({ center: [-2.5, 0.0, 0.0], radius: 0.5 }); +const sphere2 = vtkSphere.newInstance({ center: [2.5, 0.0, 0.0], radius: 0.5 }); +// const plane = vtkPlane.newInstance({ origin: [0, 0, 0], normal: [0, 1, 0] }); +const impBool = vtkImplicitBoolean.newInstance({ operation: Operation.UNION, functions: [sphere, sphere2] }); +const sample = vtkSampleFunction.newInstance({ implicitFunction: impBool, sampleDimensions: [5, 3, 3], modelBounds: [-5.0, 5.0, -1.0, 1.0, -1.0, 1.0] }); + +// Isocontour +const mSquares = vtkImageMarchingSquares.newInstance({ slice: 1 }); + +// Connect the pipeline proper +mSquares.setInputConnection(sample.getOutputPort()); +mapper.setInputConnection(mSquares.getOutputPort()); + +// Update the pipeline to obtain metadata (range) about scalars +sample.update(); +const cValues = []; +const [min, max] = sample.getOutputData().getPointData().getScalars().getRange(); +for (let i = 0; i < 20; ++i) { + cValues[i] = min + ((i / 19) * (max - min)); +} +mSquares.setContourValues(cValues); + +// Create an outline +const outline = vtkOutlineFilter.newInstance(); +outline.setInputConnection(sample.getOutputPort()); +const outlineMapper = vtkMapper.newInstance(); +outlineMapper.setInputConnection(outline.getOutputPort()); +const outlineActor = vtkActor.newInstance(); +outlineActor.setMapper(outlineMapper); +renderer.addActor(outlineActor); + +// ---------------------------------------------------------------------------- +// UI control handling +// ---------------------------------------------------------------------------- +fullScreenRenderer.addController(controlPanel); + +// Define the volume resolution +document.querySelector('.volumeResolution').addEventListener('input', (e) => { + const value = Number(e.target.value); + sample.setSampleDimensions(value, value, value); + mSquares.setSlice((value / 2.0)); + renderWindow.render(); +}); + +// Define the sphere radius +document.querySelector('.sphereRadius').addEventListener('input', (e) => { + const value = Number(e.target.value); + sphere.setRadius(value); + sphere2.setRadius(value); + sample.modified(); + renderWindow.render(); +}); + +// Indicate whether to merge conincident points or not +document.querySelector('.mergePoints').addEventListener('change', (e) => { + mSquares.setMergePoints(!!e.target.checked); + renderWindow.render(); +}); + + +// ----------------------------------------------------------- +const cam = renderer.getActiveCamera(); +cam.setFocalPoint(0, 0, 0); +cam.setPosition(0, 0, 1); +renderer.resetCamera(); +renderWindow.render(); + +// ----------------------------------------------------------- +// Make some variables global so that you can inspect and +// modify objects in your browser's developer console: +// ----------------------------------------------------------- + +global.source = sample; +global.filter = mSquares; +global.mapper = mapper; +global.actor = actor; diff --git a/Sources/Filters/General/ImageMarchingSquares/index.js b/Sources/Filters/General/ImageMarchingSquares/index.js new file mode 100644 index 00000000000..2d8f5a4cedb --- /dev/null +++ b/Sources/Filters/General/ImageMarchingSquares/index.js @@ -0,0 +1,211 @@ +import macro from 'vtk.js/Sources/macro'; +import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData'; + +import vtkCaseTable from './caseTable'; + +const { vtkErrorMacro, vtkDebugMacro } = macro; + +// ---------------------------------------------------------------------------- +// vtkImageMarchingSquares methods +// ---------------------------------------------------------------------------- + +function vtkImageMarchingSquares(publicAPI, model) { + // Set our className + model.classHierarchy.push('vtkImageMarchingSquares'); + + publicAPI.getContourValues = () => model.contourValues; + publicAPI.setContourValues = (cValues) => { + model.contourValues = cValues; + publicAPI.modified(); + }; + + const ids = []; + const pixelScalars = []; + const pixelPts = []; + const edgeMap = new Map(); + + // Retrieve scalars and pixel coordinates. i-j-k is origin of pixel. + publicAPI.getPixelScalars = (i, j, k, slice, dims, origin, spacing, s) => { + // First get the indices for the pixel + ids[0] = (k * slice) + (j * dims[0]) + i; // i, j, k + ids[1] = ids[0] + 1; // i+1, j, k + ids[2] = ids[0] + dims[0]; // i, j+1, k + ids[3] = ids[2] + 1; // i+1, j+1, k + + // Now retrieve the scalars + for (let ii = 0; ii < 4; ++ii) { + pixelScalars[ii] = s[ids[ii]]; + } + }; + + // Retrieve pixel coordinates. i-j-k is origin of pixel. + publicAPI.getPixelPoints = (i, j, k, dims, origin, spacing) => { + // (i,i+1),(j,j+1),(k,k+1) - i varies fastest; then j; then k + pixelPts[0] = origin[0] + (i * spacing[0]); // 0 + pixelPts[1] = origin[1] + (j * spacing[1]); + + pixelPts[2] = pixelPts[0] + spacing[0]; // 1 + pixelPts[3] = pixelPts[1]; + + pixelPts[4] = pixelPts[0]; // 2 + pixelPts[5] = pixelPts[1] + spacing[1]; + + pixelPts[6] = pixelPts[2]; // 3 + pixelPts[7] = pixelPts[5]; + }; + + + publicAPI.produceLines = (cVal, i, j, k, slice, dims, origin, spacing, scalars, points, lines) => { + const CASE_MASK = [1, 2, 8, 4]; // case table is actually for quad + const xyz = []; + let pId; + let tmp; + const edge = []; + + publicAPI.getPixelScalars(i, j, k, slice, dims, origin, spacing, scalars); + + let index = 0; + for (let idx = 0; idx < 4; idx++) { + if (pixelScalars[idx] >= cVal) { + index |= CASE_MASK[idx]; // eslint-disable-line no-bitwise + } + } + + const pixelLines = vtkCaseTable.getCase(index); + if (pixelLines[0] < 0) { + return; // don't get the pixel coordinates, nothing to do + } + + publicAPI.getPixelPoints(i, j, k, dims, origin, spacing); + + const z = origin[2] + (k * spacing[2]); + for (let idx = 0; pixelLines[idx] >= 0; idx += 3) { + lines.push(2); + for (let eid = 0; eid < 2; eid++) { + const edgeVerts = vtkCaseTable.getEdge(pixelLines[idx + eid]); + pId = undefined; + if (model.mergePoints) { + edge[0] = ids[edgeVerts[0]]; + edge[1] = ids[edgeVerts[1]]; + if (edge[0] > edge[1]) { + tmp = edge[0]; + edge[0] = edge[1]; + edge[1] = tmp; + } + pId = edgeMap.get(edge); + } + if (pId === undefined) { + const t = (cVal - pixelScalars[edgeVerts[0]]) / + (pixelScalars[edgeVerts[1]] - pixelScalars[edgeVerts[0]]); + const x0 = pixelPts.slice(edgeVerts[0] * 2, (edgeVerts[0] + 1) * 2); + const x1 = pixelPts.slice(edgeVerts[1] * 2, (edgeVerts[1] + 1) * 2); + xyz[0] = x0[0] + (t * (x1[0] - x0[0])); + xyz[1] = x0[1] + (t * (x1[1] - x0[1])); + pId = points.length / 3; + points.push(xyz[0], xyz[1], z); + + + if (model.mergePoints) { + edge[0] = ids[edgeVerts[0]]; + edge[1] = ids[edgeVerts[1]]; + if (edge[0] > edge[1]) { + tmp = edge[0]; + edge[0] = edge[1]; + edge[1] = tmp; + } + edgeMap[edge] = pId; + } + } + lines.push(pId); + } + } + }; + + publicAPI.requestData = (inData, outData) => { // implement requestData + const input = inData[0]; + + if (!input) { + vtkErrorMacro('Invalid or missing input'); + return; + } + + console.time('msquares'); + + // Retrieve output and volume data + const origin = input.getOrigin(); + const spacing = input.getSpacing(); + const dims = input.getDimensions(); + const s = input.getPointData().getScalars().getData(); + + // Points - dynamic array + const pBuffer = []; + + // Cells - dynamic array + const lBuffer = []; + + // Ensure slice is valid + const slice = dims[0] * dims[1]; + let k = Math.round(model.slice); + if (k >= dims[2]) { + k = 0; + } + + // Loop over all contour values, and then pixels, determine case and process + for (let cv = 0; cv < model.contourValues.length; ++cv) { + for (let j = 0; j < (dims[1] - 1); ++j) { + for (let i = 0; i < (dims[0] - 1); ++i) { + publicAPI.produceLines(model.contourValues[cv], i, j, k, slice, dims, origin, spacing, s, pBuffer, lBuffer); + } + } + edgeMap.clear(); + } + + // Update output + const polydata = vtkPolyData.newInstance(); + polydata.getPoints().setData(new Float32Array(pBuffer), 3); + polydata.getLines().setData(new Uint32Array(lBuffer)); + outData[0] = polydata; + + vtkDebugMacro('Produced output'); + console.timeEnd('msquares'); + }; +} + +// ---------------------------------------------------------------------------- +// Object factory +// ---------------------------------------------------------------------------- + +const DEFAULT_VALUES = { + contourValues: [], + slice: 0, + mergePoints: false, +}; + +// ---------------------------------------------------------------------------- + +export function extend(publicAPI, model, initialValues = {}) { + Object.assign(model, DEFAULT_VALUES, initialValues); + + // Make this a VTK object + macro.obj(publicAPI, model); + + // Also make it an algorithm with one input and one output + macro.algo(publicAPI, model, 1, 1); + + macro.setGet(publicAPI, model, [ + 'slice', + 'mergePoints', + ]); + + // Object specific methods + macro.algo(publicAPI, model, 1, 1); + vtkImageMarchingSquares(publicAPI, model); +} + +// ---------------------------------------------------------------------------- + +export const newInstance = macro.newInstance(extend, 'vtkImageMarchingSquares'); + +// ---------------------------------------------------------------------------- + +export default { newInstance, extend }; diff --git a/Sources/Filters/General/index.js b/Sources/Filters/General/index.js index 590f1011bc6..d708710b8e3 100644 --- a/Sources/Filters/General/index.js +++ b/Sources/Filters/General/index.js @@ -1,5 +1,6 @@ import vtkCalculator from './Calculator'; import vtkImageMarchingCubes from './ImageMarchingCubes'; +import vtkImageMarchingSquares from './ImageMarchingSquares'; import vtkImageStreamline from './ImageStreamline'; import vtkMoleculeToRepresentation from './MoleculeToRepresentation'; import vtkOutlineFilter from './OutlineFilter'; @@ -8,6 +9,7 @@ import vtkWarpScalar from './WarpScalar'; export default { vtkCalculator, vtkImageMarchingCubes, + vtkImageMarchingSquares, vtkImageStreamline, vtkMoleculeToRepresentation, vtkOutlineFilter,