Skip to content

Commit

Permalink
Merge pull request #106 from Kitware/datasets-and-image-array-mapper
Browse files Browse the repository at this point in the history
Datasets and image array mapper
  • Loading branch information
floryst authored Apr 18, 2023
2 parents a55e689 + a1fe1ea commit 22a4948
Show file tree
Hide file tree
Showing 8 changed files with 377 additions and 83 deletions.
255 changes: 221 additions & 34 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"dev": "rollup -c --watch"
},
"peerDependencies": {
"@kitware/vtk.js": "^26.5.3",
"@kitware/vtk.js": "^26.5.5",
"react": "^16.0.0"
},
"devDependencies": {
Expand All @@ -41,7 +41,7 @@
"@babel/preset-env": "^7.19.1",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@kitware/vtk.js": "^26.5.5",
"@kitware/vtk.js": "^26.9.9",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-commonjs": "24.0.1",
"@rollup/plugin-eslint": "^9.0.3",
Expand Down
26 changes: 26 additions & 0 deletions src/core/Dataset.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { vtkObject } from '@kitware/vtk.js/interfaces';
import { useEffect } from 'react';
import { useDownstream, useRepresentation } from './contexts';

export interface DatasetProps {
dataset: vtkObject | null;
}

export default function Dataset(props: DatasetProps) {
const representation = useRepresentation();
const downstream = useDownstream();

const { dataset } = props;

useEffect(() => {
if (!dataset) {
return;
}

downstream.setInputData(dataset);
representation.dataAvailable();
representation.dataChanged();
}, [dataset, downstream, representation]);

return null;
}
75 changes: 67 additions & 8 deletions src/core/SliceRepresentation.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import AbstractImageMapper, {
vtkAbstractImageMapper,
} from '@kitware/vtk.js/Rendering/Core/AbstractImageMapper';
import vtkImageArrayMapper from '@kitware/vtk.js/Rendering/Core/ImageArrayMapper';
import vtkImageMapper, {
IImageMapperInitialValues,
} from '@kitware/vtk.js/Rendering/Core/ImageMapper';
Expand All @@ -9,6 +13,7 @@ import { Vector2 } from '@kitware/vtk.js/types';
import {
forwardRef,
PropsWithChildren,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
Expand Down Expand Up @@ -38,6 +43,11 @@ export interface SliceRepresentationProps extends PropsWithChildren {
*/
mapper?: IImageMapperInitialValues;

/**
* An opational mapper instanc
*/
mapperInstance?: AbstractImageMapper;

/**
* Properties to set to the slice/actor
*/
Expand Down Expand Up @@ -94,6 +104,18 @@ const DefaultProps = {
colorDataRange: 'auto' as const,
};

function isVtkImageMapper(
mapper: vtkAbstractImageMapper
): mapper is vtkImageMapper {
return mapper.isA('vtkImageMapper');
}

function isVtkImageArrayMapper(
mapper: vtkAbstractImageMapper
): mapper is vtkImageArrayMapper {
return mapper.isA('vtkImageArrayMapper');
}

export default forwardRef(function SliceRepresentation(
props: SliceRepresentationProps,
fwdRef
Expand All @@ -117,12 +139,20 @@ export default forwardRef(function SliceRepresentation(

// --- mapper --- //

const getMapper = useMapper(
const getInternalMapper = useMapper(
() => vtkImageMapper.newInstance(),
props.mapper,
trackModified
);

const { mapperInstance } = props;
const getMapper = useCallback<() => vtkAbstractImageMapper>(() => {
if (mapperInstance) {
return mapperInstance;
}
return getInternalMapper();
}, [mapperInstance, getInternalMapper]);

// --- actor --- //

const actorProps = {
Expand All @@ -137,7 +167,8 @@ export default forwardRef(function SliceRepresentation(
});

useEffect(() => {
getActor().setMapper(getMapper());
// workaround for vtkImageSlice.setMapper only taking vtkImageMapper
getActor().setMapper(getMapper() as vtkImageMapper);
}, [getActor, getMapper]);

useEffect(() => {
Expand All @@ -160,30 +191,58 @@ export default forwardRef(function SliceRepresentation(

const { iSlice, jSlice, kSlice, xSlice, ySlice, zSlice } = props;

// --- vtkImageMapper setSlice --- //

useEffect(() => {
if (iSlice != null) trackModified(getMapper().setISlice(iSlice));
const mapper = getMapper();
if (isVtkImageMapper(mapper) && iSlice != null)
trackModified(mapper.setISlice(iSlice));
}, [iSlice, getMapper, trackModified]);

useEffect(() => {
if (jSlice != null) trackModified(getMapper().setJSlice(jSlice));
const mapper = getMapper();
if (isVtkImageMapper(mapper) && jSlice != null)
trackModified(mapper.setJSlice(jSlice));
}, [jSlice, getMapper, trackModified]);

useEffect(() => {
if (kSlice != null) trackModified(getMapper().setKSlice(kSlice));
const mapper = getMapper();
if (isVtkImageMapper(mapper) && kSlice != null)
trackModified(mapper.setKSlice(kSlice));
}, [kSlice, getMapper, trackModified]);

useEffect(() => {
if (xSlice != null) trackModified(getMapper().setXSlice(xSlice));
const mapper = getMapper();
if (isVtkImageMapper(mapper) && xSlice != null)
trackModified(mapper.setXSlice(xSlice));
}, [xSlice, getMapper, trackModified]);

useEffect(() => {
if (ySlice != null) trackModified(getMapper().setYSlice(ySlice));
const mapper = getMapper();
if (isVtkImageMapper(mapper) && ySlice != null)
trackModified(mapper.setYSlice(ySlice));
}, [ySlice, getMapper, trackModified]);

useEffect(() => {
if (zSlice != null) trackModified(getMapper().setZSlice(zSlice));
const mapper = getMapper();
if (isVtkImageMapper(mapper) && zSlice != null)
trackModified(mapper.setZSlice(zSlice));
}, [zSlice, getMapper, trackModified]);

// --- vtkImageArrayMapper setSlice --- //

useEffect(() => {
const mapper = getMapper();
if (
isVtkImageArrayMapper(mapper) &&
kSlice != null &&
kSlice !== mapper.getSlice()
) {
trackModified(true);
mapper.setSlice(kSlice);
}
}, [kSlice, getMapper, trackModified]);

// --- //

const renderer = useRendererContext();
Expand Down
2 changes: 1 addition & 1 deletion src/core/View.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default forwardRef(function View(props: ViewProps, fwdRef) {

const multiViewRoot = useContext(MultiViewRootContext);

const api = useMemo<IView | null>(() => {
const api = useMemo<IView>(() => {
const getView = () =>
multiViewRoot ? parentedViewRef.current : singleViewRef.current;
return {
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export { default as CellData } from './core/CellData';
export * as Contexts from './core/contexts';
export { default as DataArray } from './core/DataArray';
export type { DataArrayProps } from './core/DataArray';
export { default as Dataset } from './core/Dataset';
export type { DatasetProps } from './core/Dataset';
export { default as FieldData } from './core/FieldData';
export { default as Geometry2DRepresentation } from './core/Geometry2DRepresentation';
export type { Geometry2DRepresentationProps } from './core/Geometry2DRepresentation';
Expand Down
4 changes: 4 additions & 0 deletions usage/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ const demos = new Map([
// ],
// ['Volume/VolumeRendering', lazy(() => import('./Volume/VolumeRendering'))],
// ['Volume/DynamicUpdate', lazy(() => import('./Volume/DynamicUpdate'))],
[
'Volume/ImageSeriesRendering',
lazy(() => import('./Volume/ImageSeriesRendering')),
],
['Tests/PropertyUpdate', lazy(() => import('./Tests/PropertyUpdate'))],
['Tests/CameraTest', lazy(() => import('./Tests/CameraTest'))],
['Tests/ShareGeometry', lazy(() => import('./Tests/ShareGeometry'))],
Expand Down
92 changes: 54 additions & 38 deletions usage/src/Volume/ImageSeriesRendering.jsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import React, { useState, useContext, useEffect } from 'react';
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps.js';
import vtkCollection from '@kitware/vtk.js/Common/DataModel/Collection';
import vtkImageArrayMapper from '@kitware/vtk.js/Rendering/Core/ImageArrayMapper.js';
import vtkResourceLoader from '@kitware/vtk.js/IO/Core/ResourceLoader';
import vtkITKHelper from '@kitware/vtk.js/Common/DataModel/ITKHelper';
import vtkLiteHttpDataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper/LiteHttpDataAccessHelper';
import vtkResourceLoader from '@kitware/vtk.js/IO/Core/ResourceLoader';
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps.js';
import vtkImageArrayMapper from '@kitware/vtk.js/Rendering/Core/ImageArrayMapper.js';
import { unzipSync } from 'fflate';
import vtkITKHelper from '@kitware/vtk.js/Common/DataModel/ITKHelper';
import { useContext, useEffect, useMemo, useState } from 'react';

import {
View,
Contexts,
Dataset,
ShareDataSet,
RegisterDataSet,
ShareDataSetRoot,
SliceRepresentation,
Contexts,
UseDataSet,
View,
} from 'react-vtk-js';

function Slider(props) {
Expand Down Expand Up @@ -110,13 +112,11 @@ function CheckBox(props) {
);
}


const loadData = async () => {
console.log('Loading itk module...');
loadData.setStatusText('Loading itk module...');
if(!window.itk) {
await vtkResourceLoader
.loadScript(
if (!window.itk) {
await vtkResourceLoader.loadScript(
'https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/itk-wasm.js'
);
}
Expand Down Expand Up @@ -144,7 +144,7 @@ const loadData = async () => {

// Read individual dcm files into an array of vtkImageData.
const imageArray = [];
if(window.itk) {
if (window.itk) {
await Promise.all(
dcmFiles.map(async (filename, index) => {
const { image: itkImage, webWorker } =
Expand All @@ -167,12 +167,13 @@ const loadData = async () => {
collection.addItem(img);
}
const totalSlices = imageArray.reduce(
(accumulator, currImage) => currImage.getDimensions()[2] + accumulator, 0
(accumulator, currImage) => currImage.getDimensions()[2] + accumulator,
0
);
loadData.setMaxSlicingValue(totalSlices - 1);
loadData.setStatusText('');
return collection;
}
};

function Example(props) {
const [statusText, setStatusText] = useState('Loading data, please wait ...');
Expand All @@ -187,34 +188,50 @@ function Example(props) {
loadData.setMaxSlicingValue = setMaxKSlice;
loadData.setStatusText = setStatusText;

useEffect(
() => {
const img = mapper.getImage(kSlice);
const range = img?.getPointData()?.getScalars()?.getRange();
if(range && range.length == 2) {
const maxWidth = range[1] - range[0];
setColorWindow(maxWidth);
const center = Math.round((range[0] + range[1]) / 2);
setColorLevel(center);
}
},
[kSlice]
const [imageCollection, setImageCollection] = useState(null);

useEffect(() => {
loadData().then((ds) => {
window.ds = ds;
setImageCollection(ds);
});
}, []);

useEffect(() => {
const img = mapper.getImage(kSlice);
const range = img?.getPointData()?.getScalars()?.getRange();
if (range && range.length == 2) {
const maxWidth = range[1] - range[0];
setColorWindow(maxWidth);
const center = Math.round((range[0] + range[1]) / 2);
setColorLevel(center);
}
}, [kSlice, mapper]);

const cameraParams = useMemo(
() => ({
position: [400, 400, -1000],
viewUp: [0, -1, 0],
viewAngle: 75,
directionOfProjection: [0, 0, 1],
clippingRange: [-100, 100],
parallelProjection: false,
}),
[]
);

return (
<div style={{ width: '100%', height: '100%' }}>
<ShareDataSetRoot>
<RegisterDataSet id='mixedImages'>
<Dataset dataset={imageCollection} />
</RegisterDataSet>
<View
id='0'
cameraPosition={[0, 0, -1]}
cameraViewUp={[0, -1, 0]}
cameraParallelProjection={false}
autoResetCamera={false}
camera={cameraParams}
background={[65 / 255, 86 / 255, 122 / 255]}
>
<ShareDataSet
name='mixedImages'
>
<Dataset fetchData={loadData} />
</ShareDataSet>
<label
style={{
position: 'absolute',
Expand Down Expand Up @@ -269,11 +286,10 @@ function Example(props) {
}}
colorMapPreset={colorPreset}
>
<ShareDataSet
name='mixedImages'
/>
<UseDataSet id='mixedImages' />
</SliceRepresentation>
</View>
</ShareDataSetRoot>
</div>
);
}
Expand Down

0 comments on commit 22a4948

Please sign in to comment.