Skip to content

Commit

Permalink
feat: added threshold textfields
Browse files Browse the repository at this point in the history
  • Loading branch information
nicoalee committed Jan 14, 2025
1 parent e554149 commit 1d9575a
Show file tree
Hide file tree
Showing 5 changed files with 480 additions and 100 deletions.
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import { Box, Button, Checkbox, Link, Slider, Typography } from '@mui/material';
import { ChangeEvent, useEffect, useRef, useState } from 'react';
import { Box, Button, Checkbox, Link, Slider, TextField, Typography } from '@mui/material';
import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react';
import { Niivue, SHOW_RENDER } from '@niivue/niivue';
import { Download, OpenInNew } from '@mui/icons-material';
import ImageIcon from '@mui/icons-material/Image';
import ThresholdSlider from './ThresholdSlider';
import StateHandlerComponent from 'components/StateHandlerComponent/StateHandlerComponent';

let niivue: Niivue;
let thresholdDebounce: NodeJS.Timeout;

const NiiVueVisualizer: React.FC<{ file: string; filename: string; neurovaultLink?: string }> = ({
file,
filename,
neurovaultLink,
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [softThreshold, setSoftThresold] = useState(true);
const niivueRef = useRef<Niivue | null>(null);
const [softThreshold, setSoftThreshold] = useState(true);
const [showNegatives, setShowNegatives] = useState(false);
const [disableNegatives, setDisableNegatives] = useState(false);
const [showCrosshairs, setShowCrosshairs] = useState(true);
const [brainCoordinateString, setBrainCoordinateString] = useState('');
const [isLoading, setIsLoading] = useState(false);

const [threshold, setThreshold] = useState<{
min: number;
max: number;
Expand All @@ -25,155 +32,257 @@ const NiiVueVisualizer: React.FC<{ file: string; filename: string; neurovaultLin
value: 3,
});

const handleUpdateThreshold = (event: Event, newValue: number | number[]) => {
if (!niivue) return;
const typedVal = newValue as number;
setThreshold((prev) => ({
...prev,
value: typedVal,
}));
const handleChangeLocation = (location: unknown) => {
const typedLocation = location as {
axCorSage: number;
frac: Float32Array;
mm: Float32Array;
string: string;
values: { id: string; mm: Float32Array; name: string; value: number; vox: number[] }[];
vox: Float32Array;
xy: number[];
};

// update threshold positive
niivue.volumes[1].cal_min = typedVal;
const fileValues = typedLocation?.values?.[1];
if (!fileValues) return;
const [x, y, z] = fileValues?.mm || [];
const value = fileValues?.value;

// update threshold negative
niivue.volumes[1].cal_maxNeg = -1 * typedVal;
const str = `X: ${Math.round(x)} | Y: ${Math.round(y)} | Z: ${Math.round(z)} = ${value.toFixed(3)}`;
setBrainCoordinateString(str);
};

const updateSoftThresholdInNiivue = (softThresholdEnabled: boolean) => {
if (!niivueRef.current) return;

niivue.updateGLVolume();
if (softThresholdEnabled) {
niivueRef.current.overlayOutlineWidth = 2;
niivueRef.current.volumes[1].alphaThreshold = 5;
} else {
niivueRef.current.overlayOutlineWidth = 0;
niivueRef.current.volumes[1].alphaThreshold = 0;
}
niivueRef.current.updateGLVolume();
};

const handleToggleSoftThreshold = (event: ChangeEvent<HTMLInputElement>, checked: boolean) => {
if (!niivue) return;
setSoftThreshold(checked);
updateSoftThresholdInNiivue(checked);
};

setSoftThresold(checked);
if (checked) {
niivue.overlayOutlineWidth = 2;
niivue.volumes[1].alphaThreshold = 5;
const updateCrosshairsInNiivue = (showCrosshairsEnabled: boolean) => {
if (!niivueRef.current) return;
if (showCrosshairsEnabled) {
niivueRef.current.setCrosshairWidth(1);
} else {
niivue.overlayOutlineWidth = 0;
niivue.volumes[1].alphaThreshold = 0;
niivueRef.current.setCrosshairWidth(0);
}
niivue.updateGLVolume();
niivueRef.current.updateGLVolume();
};

const handleToggleShowCrosshairs = (event: ChangeEvent<HTMLInputElement>, checked: boolean) => {
if (!niivue) return;
setShowCrosshairs(checked);
if (checked) {
niivue.setCrosshairWidth(1);
updateCrosshairsInNiivue(checked);
};

const updateNegativesInNiivue = (showNegativesEnabled: boolean) => {
if (!niivueRef.current) return;

if (showNegativesEnabled) {
niivueRef.current.volumes[1].colormapNegative = 'winter';
} else {
niivue.setCrosshairWidth(0);
niivueRef.current.volumes[1].colormapNegative = '';
}
niivue.updateGLVolume();
niivueRef.current.updateGLVolume();
};

const handleToggleNegatives = (event: ChangeEvent<HTMLInputElement>, checked: boolean) => {
if (!niivue) return;
setShowNegatives(checked);
if (checked) {
niivue.volumes[1].colormapNegative = 'winter';
} else {
niivue.volumes[1].colormapNegative = '';
}
niivue.updateGLVolume();
updateNegativesInNiivue(checked);
};

useEffect(() => {
if (!canvasRef.current) return;

const volumes = [
{
// TODO: need to check if TAL vs MNI and set accordingly
url: 'https://neurovault.org/static/images/GenericMNI.nii.gz',
// url: 'https://niivue.github.io/niivue/images/fslmean.nii.gz',
colormap: 'gray',
opacity: 1,
},
{
const updateNiivue = async () => {
if (!canvasRef.current) return;

// this should only run once initially to load the niivue instance as well as a base image
if (niivueRef.current === null) {
niivueRef.current = new Niivue({
show3Dcrosshair: true,
});
niivueRef.current.attachToCanvas(canvasRef.current);
niivueRef.current.overlayOutlineWidth = 2;
niivueRef.current.opts.multiplanarShowRender = SHOW_RENDER.ALWAYS;
niivueRef.current.opts.isColorbar = true;
niivueRef.current.setSliceMM(false);
niivueRef.current.onLocationChange = handleChangeLocation;
await niivueRef.current.addVolumeFromUrl({
// we can assume that maps will only be in MNI space
url: 'https://neurovault.org/static/images/GenericMNI.nii.gz',
colormap: 'gray',
opacity: 1,
colorbarVisible: false,
});
}

const niivue = niivueRef.current;
await niivueRef.current.addVolumeFromUrl({
url: file,
// url: 'https://niivue.github.io/niivue/images/fslt.nii.gz',
colorMap: 'warm',
colormap: 'warm',
cal_min: 0, // default
cal_max: 6, // default
cal_minNeg: -6, // default
cal_maxNeg: 0, // default
opacity: 1,
},
];

niivue = new Niivue({
show3Dcrosshair: true,
});

niivue.opts.isColorbar = true;
niivue.setSliceMM(false);

niivue.attachToCanvas(canvasRef.current);
niivue.addVolumesFromUrl(volumes).then(() => {
niivue.overlayOutlineWidth = 2;
niivue.volumes[1].alphaThreshold = 5;

niivue.volumes[0].colorbarVisible = false;
niivue.volumes[1].colormapNegative = '';

niivue.opts.multiplanarShowRender = SHOW_RENDER.ALWAYS;
});

const globalMax = niivue.volumes[1].global_max || 6;
const globalMax = niivue.volumes[1].global_max || 2.58;
const globalMin = niivue.volumes[1].global_min || 0;
const largestAbsoluteValue = Math.max(Math.abs(globalMin), globalMax);
const startingValue = largestAbsoluteValue < 2.58 ? largestAbsoluteValue : 2.58;

updateCrosshairsInNiivue(showCrosshairs); // update crosshair settings in case they have been updated in other maps
updateSoftThresholdInNiivue(softThreshold); // update threshold settings in case they have been updated in other maps
// update negative settings in case they have been updated in other maps. If no negatives, disable
if (globalMin < 0) {
setShowNegatives(false);
setDisableNegatives(false);
updateNegativesInNiivue(false);
} else {
setShowNegatives(false);
setDisableNegatives(true);
updateNegativesInNiivue(false);
}

let startingValue;
let maxOrThreshold;
if (filename.startsWith('z_')) {
startingValue = 2.58;
maxOrThreshold = largestAbsoluteValue < 2.58 ? 2.58 : largestAbsoluteValue;
} else {
startingValue = 0;
maxOrThreshold = largestAbsoluteValue;
}

setThreshold({
min: 0,
max: Math.round((largestAbsoluteValue + 0.1) * 100) / 100,
value: startingValue,
max: Math.round(maxOrThreshold * 100) / 100,
value: Math.round(startingValue * 100) / 100,
});

niivue.volumes[1].cal_min = startingValue;
niivue.volumes[1].cal_max = largestAbsoluteValue + 0.1;
niivue.volumes[1].cal_max = maxOrThreshold;

niivue.setInterpolation(true);
niivue.updateGLVolume();
});
}, [file]);
};

updateNiivue();

return () => {
if (niivueRef.current && niivueRef.current.volumes[1]) {
niivueRef.current.removeVolume(niivueRef.current.volumes[1]);
}
};
}, [file, filename]);

const handleDownloadImage = () => {
if (!niivue) return;
niivue.saveScene(filename + '.png');
if (!niivueRef.current) return;
niivueRef.current.saveScene(filename + '.png');
};

const updateThresholdNiivue = (update: { thresholdValue: number; thresholdMax: number; thresholdMin: number }) => {
if (!niivueRef.current) return;

// update threshold positive
niivueRef.current.volumes[1].cal_min = update.thresholdValue;
// update threshold negative
niivueRef.current.volumes[1].cal_minNeg = -1 * update.thresholdValue;

niivueRef.current.volumes[1].cal_max = update.thresholdMax;
niivueRef.current.volumes[1].cal_maxNeg = -1 * update.thresholdMax;

niivueRef.current.updateGLVolume();
};

const handleUpdateThreshold = useCallback(
(update: { thresholdValue: number; thresholdMax: number; thresholdMin: number }) => {
setThreshold({
min: update.thresholdMin,
max: update.thresholdMax,
value: update.thresholdValue,
});

updateThresholdNiivue(update);
},
[]
);

return (
<Box>
{/* <StateHandlerComponent isLoading={isLoading} isError={false}> */}
<Box sx={{ marginBottom: '10px', display: 'flex', justifyContent: 'space-between' }}>
<Box width="250px">
<Typography gutterBottom={false}>Threshold</Typography>
<Slider
valueLabelDisplay="auto"
min={threshold.min}
step={0.01}
max={threshold.max}
onChange={handleUpdateThreshold}
value={threshold.value}
></Slider>
<ThresholdSlider
thresholdMin={threshold.min}
thresholdMax={threshold.max}
threshold={threshold.value}
onDebouncedThresholdChange={handleUpdateThreshold}
/>
</Box>
<Box display="flex">
<Box width="100px">
<Typography gutterBottom={false}>Soft Threshold</Typography>
<Checkbox checked={softThreshold} onChange={handleToggleSoftThreshold} />
<Box width="130px" display="flex" flexDirection="column">
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="caption" gutterBottom={false}>
Soft Threshold
</Typography>
<Checkbox sx={{ padding: 0 }} checked={softThreshold} onChange={handleToggleSoftThreshold} />
</Box>
<Box width="100px">
<Typography gutterBottom={false}>Show Negatives</Typography>
<Checkbox checked={showNegatives} onChange={handleToggleNegatives} />
</Box>
<Box width="100px">
<Typography gutterBottom={false}>Show Crosshairs</Typography>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="caption" gutterBottom={false}>
Show Crosshairs
</Typography>
<Checkbox
sx={{ padding: 0 }}
value={showCrosshairs}
checked={showCrosshairs}
onChange={handleToggleShowCrosshairs}
/>
</Box>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography
variant="caption"
color={disableNegatives ? 'muted.main' : 'inherit'}
gutterBottom={false}
>
{disableNegatives ? 'No Negatives' : 'Show Negatives'}
{/* Show Negatives */}
</Typography>
<Checkbox
sx={{ padding: 0 }}
disabled={disableNegatives}
checked={showNegatives}
onChange={handleToggleNegatives}
/>
</Box>
</Box>
</Box>
{/* </StateHandlerComponent> */}
<Box sx={{ height: '32px' }}>
{brainCoordinateString && (
<Box
sx={{
width: '260px',
backgroundColor: 'black',
textAlign: 'center',
borderTopLeftRadius: '4px',
borderTopRightRadius: '4px',
}}
>
<Typography padding="4px 8px" display="inline-block" color="white">
{brainCoordinateString}
</Typography>
</Box>
)}
</Box>
<Box sx={{ height: '300px' }}>
<canvas ref={canvasRef} />
</Box>
Expand Down
Loading

0 comments on commit 1d9575a

Please sign in to comment.