Skip to content

Commit 6993c68

Browse files
committed
fix(ImageCroppingRegionsWidget): Add image cropping widget
1 parent f8d8e6b commit 6993c68

File tree

5 files changed

+912
-0
lines changed

5 files changed

+912
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const Orientation = {
2+
YZ: 0,
3+
XZ: 1,
4+
XY: 2,
5+
};
6+
7+
export default { Orientation };
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
import macro from 'vtk.js/Sources/macro';
2+
import vtkActor from 'vtk.js/Sources/Rendering/Core/Actor';
3+
import vtkCellPicker from 'vtk.js/Sources/Rendering/Core/CellPicker';
4+
import vtkWidgetRepresentation from 'vtk.js/Sources/Interaction/Widgets/WidgetRepresentation';
5+
import vtkMapper from 'vtk.js/Sources/Rendering/Core/Mapper';
6+
import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData';
7+
import Constants from 'vtk.js/Sources/Interaction/Widgets/ImageCroppingRegionsRepresentation/Constants';
8+
9+
const { vtkErrorMacro } = macro;
10+
const { Orientation } = Constants;
11+
12+
// ----------------------------------------------------------------------------
13+
// vtkImageCroppingRegionsRepresentation methods
14+
// ----------------------------------------------------------------------------
15+
16+
function vtkImageCroppingRegionsRepresentation(publicAPI, model) {
17+
// Set our className
18+
model.classHierarchy.push('vtkImageCroppingRegionsRepresentation');
19+
20+
// this is called at the end of the enclosing function.
21+
function constructor() {
22+
// set fields from parent classes
23+
model.placeFactor = 1;
24+
25+
model.initialBounds = [0, 1, 0, 1, 0, 1];
26+
// Format:
27+
// [xmin, xmax, ymin, ymax, zmin, zmax]
28+
model.planePositions = [0, 0, 0, 0, 0, 0];
29+
model.sliceOrientation = Orientation.XY;
30+
model.slice = 0;
31+
32+
// construct region polydata
33+
model.regionPolyData = vtkPolyData.newInstance();
34+
model.regionPolyData.getPoints().setData(new Float32Array(16 * 3), 3);
35+
/*
36+
15-14-----11-10
37+
| |
38+
12 13-----8 9
39+
| | | |
40+
3 2------7 6
41+
| |
42+
0--1------4--5
43+
*/
44+
// set polys
45+
model.regionPolyData.getPolys().setData(new Uint32Array([
46+
4, 0, 1, 2, 3,
47+
4, 1, 4, 7, 2,
48+
4, 4, 5, 6, 7,
49+
4, 7, 6, 9, 8,
50+
4, 8, 9, 10, 11,
51+
4, 13, 8, 11, 14,
52+
4, 12, 13, 14, 15,
53+
4, 3, 2, 13, 12,
54+
]), 1);
55+
56+
model.mapper = vtkMapper.newInstance();
57+
model.actor = vtkActor.newInstance();
58+
59+
model.mapper.setInputData(model.regionPolyData);
60+
model.actor.setMapper(model.mapper);
61+
model.actor.getProperty().setEdgeColor(1.0, 0, 0);
62+
model.actor.getProperty().setEdgeVisibility(true);
63+
publicAPI.setOpacity(model.opacity);
64+
65+
// set picker
66+
model.cursorPicker = vtkCellPicker.newInstance();
67+
}
68+
69+
publicAPI.getActors = () => model.actor;
70+
publicAPI.getNestedProps = () => publicAPI.getActors();
71+
72+
// Reorders a bounds array such that each (a,b) pairing is a
73+
// (min,max) pairing.
74+
function reorderBounds(bounds) {
75+
for (let i = 0; i < 6; i += 2) {
76+
if (bounds[i] > bounds[i + 1]) {
77+
const tmp = bounds[i + 1];
78+
bounds[i + 1] = bounds[i];
79+
bounds[i] = tmp;
80+
}
81+
}
82+
}
83+
84+
publicAPI.placeWidget = (...bounds) => {
85+
const boundsArray = [];
86+
87+
for (let i = 0; i < bounds.length; i++) {
88+
boundsArray.push(bounds[i]);
89+
}
90+
91+
if (boundsArray.length !== 6) {
92+
return;
93+
}
94+
95+
// make sure each bounds pairing is monotonic
96+
reorderBounds(boundsArray);
97+
98+
const newBounds = [];
99+
const center = [];
100+
publicAPI.adjustBounds(boundsArray, newBounds, center);
101+
102+
for (let i = 0; i < 6; i++) {
103+
model.initialBounds[i] = newBounds[i];
104+
}
105+
106+
model.initialLength = Math.sqrt(
107+
((newBounds[1] - newBounds[0]) * (newBounds[1] - newBounds[0])) +
108+
((newBounds[3] - newBounds[2]) * (newBounds[3] - newBounds[2])) +
109+
((newBounds[5] - newBounds[4]) * (newBounds[5] - newBounds[4])));
110+
111+
// set plane positions
112+
publicAPI.setPlanePositions(...model.initialBounds);
113+
};
114+
115+
publicAPI.setSlice = (slice, sliceOrientation) => {
116+
model.slice = slice;
117+
model.sliceOrientation = sliceOrientation;
118+
publicAPI.updateGeometry();
119+
};
120+
121+
publicAPI.setOpacity = (opacityValue) => {
122+
const opacity = Math.max(0, Math.min(1, opacityValue));
123+
model.actor.getProperty().setOpacity(opacity);
124+
};
125+
126+
publicAPI.setPlanePositions = (...positions) => {
127+
if (positions.length !== 6) {
128+
vtkErrorMacro('setPlanePositions() must be given a 3D boundaries array');
129+
return;
130+
}
131+
132+
// swap around plane boundaries if not in order
133+
reorderBounds(positions);
134+
135+
// make sure each position is within the bounds
136+
for (let i = 0; i < 6; i += 2) {
137+
if (positions[i] < model.initialBounds[i] || positions[i] > model.initialBounds[i + 1]) {
138+
positions[i] = model.initialBounds[i];
139+
}
140+
if (positions[i + 1] < model.initialBounds[i] || positions[i + 1] > model.initialBounds[i + 1]) {
141+
positions[i + 1] = model.initialBounds[i + 1];
142+
}
143+
}
144+
145+
// set plane positions
146+
model.planePositions = positions;
147+
publicAPI.updateGeometry();
148+
};
149+
150+
publicAPI.updateGeometry = () => {
151+
const slicePos = model.slice;
152+
const verts = model.regionPolyData.getPoints().getData();
153+
const [xBMin, xBMax, yBMin, yBMax, zBMin, zBMax] = model.initialBounds;
154+
const [xPLower, xPUpper, yPLower, yPUpper, zPLower, zPUpper] = model.planePositions;
155+
156+
// set vert coordinates depending on slice orientation
157+
switch (model.sliceOrientation) {
158+
case Orientation.YZ:
159+
verts.set([slicePos, yBMin, zBMin], 0);
160+
verts.set([slicePos, yPLower, zBMin], 3);
161+
verts.set([slicePos, yPLower, zPLower], 6);
162+
verts.set([slicePos, yBMin, zPLower], 9);
163+
verts.set([slicePos, yPUpper, zBMin], 12);
164+
verts.set([slicePos, yBMax, zBMin], 15);
165+
verts.set([slicePos, yBMax, zPLower], 18);
166+
verts.set([slicePos, yPUpper, zPLower], 21);
167+
verts.set([slicePos, yPUpper, zPUpper], 24);
168+
verts.set([slicePos, yBMax, zPUpper], 27);
169+
verts.set([slicePos, yBMax, zBMax], 30);
170+
verts.set([slicePos, yPUpper, zBMax], 33);
171+
verts.set([slicePos, yBMin, zPUpper], 36);
172+
verts.set([slicePos, yPLower, zPUpper], 39);
173+
verts.set([slicePos, yPLower, zBMax], 42);
174+
verts.set([slicePos, yBMin, zBMax], 45);
175+
break;
176+
case Orientation.XZ:
177+
verts.set([xBMin, slicePos, zBMin], 0);
178+
verts.set([xPLower, slicePos, zBMin], 3);
179+
verts.set([xPLower, slicePos, zPLower], 6);
180+
verts.set([xBMin, slicePos, zPLower], 9);
181+
verts.set([xPUpper, slicePos, zBMin], 12);
182+
verts.set([xBMax, slicePos, zBMin], 15);
183+
verts.set([xBMax, slicePos, zPLower], 18);
184+
verts.set([xPUpper, slicePos, zPLower], 21);
185+
verts.set([xPUpper, slicePos, zPUpper], 24);
186+
verts.set([xBMax, slicePos, zPUpper], 27);
187+
verts.set([xBMax, slicePos, zBMax], 30);
188+
verts.set([xPUpper, slicePos, zBMax], 33);
189+
verts.set([xBMin, slicePos, zPUpper], 36);
190+
verts.set([xPLower, slicePos, zPUpper], 39);
191+
verts.set([xPLower, slicePos, zBMax], 42);
192+
verts.set([xBMin, slicePos, zBMax], 45);
193+
break;
194+
case Orientation.XY:
195+
verts.set([xBMin, yBMin, slicePos], 0);
196+
verts.set([xPLower, yBMin, slicePos], 3);
197+
verts.set([xPLower, yPLower, slicePos], 6);
198+
verts.set([xBMin, yPLower, slicePos], 9);
199+
verts.set([xPUpper, yBMin, slicePos], 12);
200+
verts.set([xBMax, yBMin, slicePos], 15);
201+
verts.set([xBMax, yPLower, slicePos], 18);
202+
verts.set([xPUpper, yPLower, slicePos], 21);
203+
verts.set([xPUpper, yPUpper, slicePos], 24);
204+
verts.set([xBMax, yPUpper, slicePos], 27);
205+
verts.set([xBMax, yBMax, slicePos], 30);
206+
verts.set([xPUpper, yBMax, slicePos], 33);
207+
verts.set([xBMin, yPUpper, slicePos], 36);
208+
verts.set([xPLower, yPUpper, slicePos], 39);
209+
verts.set([xPLower, yBMax, slicePos], 42);
210+
verts.set([xBMin, yBMax, slicePos], 45);
211+
break;
212+
default:
213+
// noop
214+
}
215+
model.regionPolyData.modified();
216+
};
217+
218+
publicAPI.getBounds = () => model.initialBounds;
219+
220+
publicAPI.buildRepresentation = () => {
221+
if (model.renderer) {
222+
if (!model.placed) {
223+
model.validPick = 1;
224+
model.placed = 1;
225+
}
226+
publicAPI.modified();
227+
}
228+
};
229+
230+
publicAPI.setProperty = (property) => {
231+
model.actor.setProperty(property);
232+
};
233+
234+
// invoke the constructor after setting up public methods
235+
constructor();
236+
}
237+
238+
// ----------------------------------------------------------------------------
239+
// Object factory
240+
// ----------------------------------------------------------------------------
241+
242+
const DEFAULT_VALUES = {
243+
constraintAxis: -1,
244+
translationMode: 1,
245+
waitingForMotion: 0,
246+
hotSpotSize: 0.05,
247+
opacity: 0.5,
248+
};
249+
250+
// ----------------------------------------------------------------------------
251+
252+
export function extend(publicAPI, model, initialValues = {}) {
253+
Object.assign(model, DEFAULT_VALUES, initialValues);
254+
255+
// Inheritance
256+
vtkWidgetRepresentation.extend(publicAPI, model, initialValues);
257+
258+
macro.setGet(publicAPI, model, ['translationMode']);
259+
260+
macro.get(publicAPI, model, [
261+
'initialBounds',
262+
'planePositions',
263+
'sliceOrientation',
264+
'slice',
265+
'opacity',
266+
]);
267+
268+
// Object methods
269+
vtkImageCroppingRegionsRepresentation(publicAPI, model);
270+
}
271+
272+
// ----------------------------------------------------------------------------
273+
274+
export const newInstance = macro.newInstance(extend, 'vtkImageCroppingRegionsRepresentation');
275+
276+
// ----------------------------------------------------------------------------
277+
278+
export default { newInstance, extend };
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const WidgetState = {
2+
IDLE: 0,
3+
MOVE_XLOWER: -1,
4+
MOVE_XUPPER: -2,
5+
MOVE_YLOWER: -3,
6+
MOVE_YUPPER: -4,
7+
MOVE_XLOWER_YLOWER: -5,
8+
MOVE_XLOWER_YUPPER: -6,
9+
MOVE_XUPPER_YLOWER: -7,
10+
MOVE_XUPPER_YUPPER: -8,
11+
12+
MOVE_LEFT: 1,
13+
MOVE_RIGHT: 2,
14+
MOVE_BOTTOM: 3,
15+
MOVE_TOP: 4,
16+
MOVE_LEFT_BOTTOM: 5,
17+
MOVE_LEFT_TOP: 6,
18+
MOVE_RIGHT_BOTTOM: 7,
19+
MOVE_RIGHT_TOP: 8,
20+
};
21+
22+
const SliceNormals = ['X', 'Y', 'Z'];
23+
24+
export default { WidgetState, SliceNormals };
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import 'vtk.js/Sources/favicon';
2+
3+
import vtkFullScreenRenderWindow from 'vtk.js/Sources/Rendering/Misc/FullScreenRenderWindow';
4+
import vtkHttpDataSetReader from 'vtk.js/Sources/IO/Core/HttpDataSetReader';
5+
import vtkImageMapper from 'vtk.js/Sources/Rendering/Core/ImageMapper';
6+
import vtkImageSlice from 'vtk.js/Sources/Rendering/Core/ImageSlice';
7+
import vtkInteractorStyleImage from 'vtk.js/Sources/Interaction/Style/InteractorStyleImage';
8+
import vtkImageCroppingRegionsWidget from 'vtk.js/Sources/Interaction/Widgets/ImageCroppingRegionsWidget';
9+
10+
// ----------------------------------------------------------------------------
11+
// Standard rendering code setup
12+
// ----------------------------------------------------------------------------
13+
14+
const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance();
15+
const renderer = fullScreenRenderer.getRenderer();
16+
const renderWindow = fullScreenRenderer.getRenderWindow();
17+
const interactorStyle2D = vtkInteractorStyleImage.newInstance();
18+
console.log(interactorStyle2D);
19+
renderWindow.getInteractor().setInteractorStyle(interactorStyle2D);
20+
renderer.getActiveCamera().setParallelProjection(true);
21+
22+
// set the current image number to the first image
23+
interactorStyle2D.setCurrentImageNumber(0);
24+
25+
// ----------------------------------------------------------------------------
26+
// Create widget
27+
// ----------------------------------------------------------------------------
28+
const widget = vtkImageCroppingRegionsWidget.newInstance();
29+
widget.setInteractor(renderWindow.getInteractor());
30+
31+
// called when the volume is loaded
32+
function setupWidget(imageMapper) {
33+
widget.setImageMapper(imageMapper);
34+
widget.setEnable(true);
35+
36+
renderWindow.render();
37+
}
38+
39+
// ----------------------------------------------------------------------------
40+
// Set up volume
41+
// ----------------------------------------------------------------------------
42+
const mapper = vtkImageMapper.newInstance();
43+
const actor = vtkImageSlice.newInstance();
44+
actor.setMapper(mapper);
45+
renderer.addViewProp(actor);
46+
47+
const reader = vtkHttpDataSetReader.newInstance({ fetchGzip: true });
48+
reader.setUrl(`${__BASE_PATH__}/data/volume/headsq.vti`, { loadData: true }).then(() => {
49+
const data = reader.getOutputData();
50+
// we don't care about image direction here
51+
data.setDirection(1, 0, 0, 0, 1, 0, 0, 0, 1);
52+
53+
mapper.setInputData(data);
54+
// change me!
55+
const sliceMode = 0;
56+
const sliceNormal = ['X', 'Y', 'Z'][sliceMode];
57+
mapper.setCurrentSlicingMode(sliceMode);
58+
mapper[`set${sliceNormal}Slice`](32);
59+
60+
const camPosition = renderer.getActiveCamera().getFocalPoint().map((v, idx) => (idx === sliceMode ? (v + 1) : v));
61+
const viewUp = [0, 0, 0];
62+
// viewUp[(sliceMode + 2) % 3] = 1;
63+
viewUp[2] = 1;
64+
renderer.getActiveCamera().set({ position: camPosition, viewUp });
65+
console.log(renderer.getActiveCamera().getViewUp());
66+
67+
// create our cropping widget
68+
setupWidget(mapper);
69+
70+
renderer.resetCamera();
71+
renderWindow.render();
72+
});
73+
74+
renderWindow.render();
75+
76+
// -----------------------------------------------------------
77+
// Make some variables global so that you can inspect and
78+
// modify objects in your browser's developer console:
79+
// -----------------------------------------------------------
80+
81+
global.renderer = renderer;
82+
global.renderWindow = renderWindow;
83+
global.widget = widget;

0 commit comments

Comments
 (0)