feat: cornerstone3D stack and volume viewports (#2787)
* feat: cs3d working stack viewport and tools (#19) * Squashed everything * fix weird react issue * fix eslint / prettier stuff * thumbnails work now with cpu rendering * feat: use new loadImageToCanvas in cs3d * make jump to measurement work * fix active thumbnail * fix measurement delete * fix window level presets * remove segmentation and sync groups for now * fix the dicom pdf and dicom video * fix cornerstone window assignment for cypress * apply review comments Co-authored-by: Erik <erik.sweed@gmail.com> * feat: cs3d tools and toolGroups (#20) * add more tools to work with cs3d mode * fix hotkeys * add stack manager usage for stach viewports * add image scrollbar * wip viewport overlay * fix toAnnotation schema for tools * fix the unnecessary size change that triggered resize * hanging protocol improvement to allow unmatched errors * study description matching for hanging protocol * fix the displaysetOptions to work * fix handle the active tool when a new viewport is added * fix separate toolGroups for mode * apply review comments * apply review comments * yarn lock * feat: overlay component (#21) * fix default displayset options * add viewport overlay * apply review comments * feat: loading and orientation indicators (#22) * add loading indicator * add orientation marker initial work * apply review comments * fix: orientation markers (#23) * finished the orientation markers * fix various broken cypress tests * apply review comments * update yarn lock * feat: re-working measurement tracking mode with cornerstone3d (#2805) * feat: cs3d working stack viewport and tools (#19) * Squashed everything * fix weird react issue * fix eslint / prettier stuff * thumbnails work now with cpu rendering * feat: use new loadImageToCanvas in cs3d * make jump to measurement work * fix active thumbnail * fix measurement delete * fix window level presets * remove segmentation and sync groups for now * fix the dicom pdf and dicom video * fix cornerstone window assignment for cypress * apply review comments Co-authored-by: Erik <erik.sweed@gmail.com> * feat: Add Measurement tracking mode with cs3D (#2789) * feat: first render for cornerstone3d tracked viewport * make tool active work * wip for SR extension * renamed dicom sr to cornerstone dicom sr * remove cornerstone from dicom pdf and video * move dicom sr logic to sr extension * feat: Add hydration for the length tool * fix SR display tool for length using cs3d * fix default config * fix: various bugs with sr viewport and tracking * fix promptying to continue tracking for when SR is created * feat: add keep trackign of unique identifiers * fix hydration for same imageIds * feat: Add SR toolGroup creation on modeEnter * feat: remove the need for separate mapper for SR hydration * add SR display for ellipse * handle hydration of elliptical ROI tool * remove cornerstone extension * add arrow mapping * feat: Add ArrowAnnotate SR display and hydration * apply review comments * apply review comments * move viewport labels to the viewportData * apply review comments * fix: integration cypress tests with cornerstone3D and add CINE tool (#2795) * fix: integration cypress tests with cornerstone3D * revert to addOrUpdate as it makes more sense * fix local drag and drop * fix tests * move dicomLoaderService to cornerstone extension * fix various import bugs * fix bug for local PT series * fix various unit tests * bump cs3d versions * add angle and magnify tool * bump dependencies to avoid broken peerDeps * feat: add initial work for capture using cs3d * feat: show annotations on the image capture * feat: add svg layer export * feat: Add CINE Tool * feat: remove unnecessary viewport rendering for cine state changes Co-authored-by: Erik Ziegler <erik.sweed@gmail.com> * docs: modify and improve documentation (#2800) * cleanup docs versionings * feat: Add all version explanations * version docs for 3.0 * wip for changing docs * wip for updated docs * add utility module documentation * fix demo with nohoisting of history * add slides and video to resources * apply review comments * fix: drag and drop SR into SR viewport (#2803) * update yarn lock Co-authored-by: Erik <erik.sweed@gmail.com> * update pathnames to match v3-stable * update the e2e pathname * fix: various bugs with regard to tracking workflow (#2811) * fix: various issues with measurement panel * fix: update default tool style for annotations * fix: annotatoin label getting removed * feat: Add backward compatibility for SR hydration with legacy cornerstone * fix: cursors and ellipse ROI max style * fix: ArrowAnnotate SRDisplay * apply review comments * bump package versions * fix: bugin rehydration of SR * fix: e2e tests * fix active viewport thickness and arrowTool ui * add readme for measurement tracking * use uploaded image for readme * add back images * try to fix e2e test * fix: window level presets hotkeys * Update README.md * update yarn lock * feat: volume api and TMTV mode (#2817) * feat: volumeAPI and TMTV mode * feat: use cs3d cache service to obtain viewportData * wip for volume api * wip: fusion viewport * feat: add blend mode option * wip for image scrollbar * fix drag and drop thumbnail * wip for image scrollbar for voluems * fix: element mismatch bug for scroll * feat: Add image scrollbar to volumes * feat: add syncGroups to volume api * feat: add tmtv mode initial setup * feat: add initial image options for the stack viewports * feat: Add custom load strategy for volume viewports via HP * apply review comments fix: Jump presets cs3d (#2812) * feat: Add JumpPreset to OHIF for Cornerstone3D * fix: accessing viewport service from servicesManager feat: volume API and TMTV mode (#2814) * fix: do not display overlays on mip viewports * feat: add optional disableCommands for toggle buttons * fix: toggleCrossharis * feat: config the crosshairs * feat: add PetSUV Panel for changing metadata * feat: initial work for the rectangleROIThreshold panel * wip: measurement service * roi threshold working * feat: Add displayText to segmentations * feat: add remove segmentation * feat: add csv export * feat: add RT export for annotations in tmtv mode * fix: fusion to use pt in tmtv mode and measuremet mappings * fix: various bugs * apply review comments * apply review comments * feat: add fusion color maps * add readme to tmtv mode * Update README.md * fix: try to fix build * fix unit tests * Update README.md * feat: add about to the panel * fix: changing strategy in roi panel * apply review comments * update package versions * wip for stackPrefetch * fix: cornerstone3d hydration and renaming (#2818) * update readme * renamed cornerstone extension * wip for renaming cornerstone3D variables * wip for renaming cornerstone3D variables * wip for fixing bugs for SR viewport * fix: jumpToMeasurement and initial label after hydration * fix: fileName capitalization * feat: use the new prefetch stack in the cs3d (#2820) * feat: use the new prefetch stack in the cs3d * use viewport scroll api for stack viewport * fix cine stop when scrollbar changes * feat: use new prefetch events * fix loading state to not show repeatedly * fix: various bugs for tmtv mode thresholding and new icons (#2823) * feat: make tmtv mode available in worklist * feat: add new icons for tmtv mode * feat: add fusion color icon * fix: parallel scale calculation * fix: bump Cornerstone to get large image support working * fix: Fix issues with CPU viewport flipping, including config files * fix: bump cornerstone to fix magnify tool * fix: Bump Cornerstone version to fix resetCamera issue in StackViewport * ci: Add _headers file to enable CORS headers for Netlify Drag/Drop deploys * fix: WADO-URI was not working. PET Metadata was coming from the wrong place. Fixed some minor React errors * feat(OHIF):Allow modes and extensions to be added after commpile time. (#2838) Also works with the compile time add that the existing cli uses, so that both build types work. Eric and I agreed this doesn't change existing functionality, but is almost entirely build issues/fixes. * bump: dependency versions to fix hydration bugs (#2848) * bump: dcmjs version to fix hydration bugs * try to fix tests * bump dependency versions Co-authored-by: Alireza <ar.sedghi@gmail.com> Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
This commit is contained in:
668 files changed
+41669
-13410
No files matched your search
@@ -0,0 +1,243 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Input, Button } from '@ohif/ui';
|
||||
import { classes, DicomMetadataStore } from '@ohif/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const DEFAULT_MEATADATA = {
|
||||
PatientWeight: null,
|
||||
PatientSex: null,
|
||||
SeriesTime: null,
|
||||
RadiopharmaceuticalInformationSequence: {
|
||||
RadionuclideTotalDose: null,
|
||||
RadionuclideHalfLife: null,
|
||||
RadiopharmaceuticalStartTime: null,
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
* PETSUV panel enables the user to modify the patient related information, such as
|
||||
* patient sex, patientWeight. This is allowed since
|
||||
* sometimes these metadata are missing or wrong. By changing them
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
export default function PanelPetSUV({ servicesManager, commandsManager }) {
|
||||
const { t } = useTranslation('PanelSUV');
|
||||
const { DisplaySetService } = servicesManager.services;
|
||||
const [metadata, setMetadata] = useState(DEFAULT_MEATADATA);
|
||||
const [ptDisplaySet, setPtDisplaySet] = useState(null);
|
||||
|
||||
const handleMetadataChange = useCallback(
|
||||
metadata => {
|
||||
setMetadata(prevState => {
|
||||
const newState = { ...prevState };
|
||||
Object.keys(metadata).forEach(key => {
|
||||
if (typeof metadata[key] === 'object') {
|
||||
newState[key] = {
|
||||
...prevState[key],
|
||||
...metadata[key],
|
||||
};
|
||||
} else {
|
||||
newState[key] = metadata[key];
|
||||
}
|
||||
});
|
||||
return newState;
|
||||
});
|
||||
},
|
||||
[metadata]
|
||||
);
|
||||
|
||||
const getMatchingPTDisplaySet = useCallback(() => {
|
||||
const ptDisplaySet = commandsManager.runCommand('getMatchingPTDisplaySet');
|
||||
|
||||
if (!ptDisplaySet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metadata = commandsManager.runCommand('getPTMetadata', {
|
||||
ptDisplaySet,
|
||||
});
|
||||
|
||||
return {
|
||||
ptDisplaySet,
|
||||
metadata,
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const displaySets = DisplaySetService.activeDisplaySets;
|
||||
|
||||
if (!displaySets.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const displaySetInfo = getMatchingPTDisplaySet();
|
||||
|
||||
if (!displaySetInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { ptDisplaySet, metadata } = displaySetInfo;
|
||||
setPtDisplaySet(ptDisplaySet);
|
||||
setMetadata(metadata);
|
||||
}, []);
|
||||
|
||||
// get the patientMetadata from the StudyInstanceUIDs and update the state
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = DisplaySetService.subscribe(
|
||||
DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
||||
() => {
|
||||
const displaySetInfo = getMatchingPTDisplaySet();
|
||||
|
||||
if (!displaySetInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { ptDisplaySet, metadata } = displaySetInfo;
|
||||
setPtDisplaySet(ptDisplaySet);
|
||||
setMetadata(metadata);
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
function updateMetadata() {
|
||||
if (!ptDisplaySet) {
|
||||
throw new Error('No ptDisplaySet found');
|
||||
}
|
||||
// metadata should be dcmjs naturalized
|
||||
DicomMetadataStore.updateMetadataForSeries(
|
||||
ptDisplaySet.StudyInstanceUID,
|
||||
ptDisplaySet.SeriesInstanceUID,
|
||||
metadata
|
||||
);
|
||||
|
||||
// update the displaySets
|
||||
DisplaySetService.setDisplaySetMetadataInvalidated(
|
||||
ptDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-hidden overflow-y-auto invisible-scrollbar">
|
||||
{
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col p-4 space-y-2 bg-primary-dark">
|
||||
<Input
|
||||
label={t('Patient Sex')}
|
||||
labelClassName="text-white"
|
||||
className="mt-1 mb-2 bg-black border-primary-main"
|
||||
type="text"
|
||||
containerClassName="mr-2"
|
||||
value={metadata.PatientSex || ''}
|
||||
onChange={e => {
|
||||
handleMetadataChange({
|
||||
PatientSex: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
label={t('Patient Weight')}
|
||||
labelClassName="text-white"
|
||||
className="mt-1 mb-2 bg-black border-primary-main"
|
||||
type="text"
|
||||
containerClassName="mr-2"
|
||||
value={metadata.PatientWeight || ''}
|
||||
onChange={e => {
|
||||
handleMetadataChange({
|
||||
PatientWeight: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
label={t('Total Dose')}
|
||||
labelClassName="text-white"
|
||||
className="mt-1 mb-2 bg-black border-primary-main"
|
||||
type="text"
|
||||
containerClassName="mr-2"
|
||||
value={
|
||||
metadata.RadiopharmaceuticalInformationSequence
|
||||
.RadionuclideTotalDose || ''
|
||||
}
|
||||
onChange={e => {
|
||||
handleMetadataChange({
|
||||
RadiopharmaceuticalInformationSequence: {
|
||||
RadionuclideTotalDose: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
label={t('Half Life')}
|
||||
labelClassName="text-white"
|
||||
className="mt-1 mb-2 bg-black border-primary-main"
|
||||
type="text"
|
||||
containerClassName="mr-2"
|
||||
value={
|
||||
metadata.RadiopharmaceuticalInformationSequence
|
||||
.RadionuclideHalfLife || ''
|
||||
}
|
||||
onChange={e => {
|
||||
handleMetadataChange({
|
||||
RadiopharmaceuticalInformationSequence: {
|
||||
RadionuclideHalfLife: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
label={t('Injection Time')}
|
||||
labelClassName="text-white"
|
||||
className="mt-1 mb-2 bg-black border-primary-main"
|
||||
type="text"
|
||||
containerClassName="mr-2"
|
||||
value={
|
||||
metadata.RadiopharmaceuticalInformationSequence
|
||||
.RadiopharmaceuticalStartTime || ''
|
||||
}
|
||||
onChange={e => {
|
||||
handleMetadataChange({
|
||||
RadiopharmaceuticalInformationSequence: {
|
||||
RadiopharmaceuticalStartTime: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
label={t('Acquisition Time')}
|
||||
labelClassName="text-white"
|
||||
className="mt-1 mb-2 bg-black border-primary-main"
|
||||
type="text"
|
||||
containerClassName="mr-2"
|
||||
value={metadata.SeriesTime || ''}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={updateMetadata}
|
||||
className="px-1 py-1 mx-4 mt-2 text-xs text-white border-b border-transparent"
|
||||
>
|
||||
Reload Data
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
PanelPetSUV.propTypes = {
|
||||
servicesManager: PropTypes.shape({
|
||||
services: PropTypes.shape({
|
||||
MeasurementService: PropTypes.shape({
|
||||
getMeasurements: PropTypes.func.isRequired,
|
||||
subscribe: PropTypes.func.isRequired,
|
||||
EVENTS: PropTypes.object.isRequired,
|
||||
VALUE_TYPES: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { Button, ButtonGroup } from '@ohif/ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function ExportReports({ segmentations, tmtvValue, config, commandsManager }) {
|
||||
const { t } = useTranslation('PanelSUVExport');
|
||||
|
||||
return (
|
||||
<>
|
||||
{segmentations?.length ? (
|
||||
<div className="flex justify-center mt-4 space-x-2">
|
||||
<ButtonGroup color="black" size="inherit">
|
||||
<Button
|
||||
className="px-2 py-2 text-base"
|
||||
disabled={tmtvValue === null}
|
||||
onClick={() => {
|
||||
commandsManager.runCommand('exportTMTVReportCSV', {
|
||||
segmentations,
|
||||
tmtv: tmtvValue,
|
||||
config,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('Export CSV')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup color="black" size="inherit">
|
||||
<Button
|
||||
className="px-2 py-2 text-base"
|
||||
onClick={() => {
|
||||
commandsManager.runCommand('createTMTVRTReport');
|
||||
}}
|
||||
disabled={tmtvValue === null}
|
||||
>
|
||||
{t('Create RT Report')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExportReports;
|
||||
+322
@@ -0,0 +1,322 @@
|
||||
import React, { useEffect, useState, useCallback, useReducer } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SegmentationTable, Button, Icon } from '@ohif/ui';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import segmentationEditHandler from './segmentationEditHandler';
|
||||
import ExportReports from './ExportReports';
|
||||
import ROIThresholdConfiguration, {
|
||||
ROI_STAT,
|
||||
} from './ROIThresholdConfiguration';
|
||||
|
||||
const LOWER_THRESHOLD_DEFAULT = 0;
|
||||
const UPPER_THRESHOLD_DEFAULT = 100;
|
||||
const WEIGHT_DEFAULT = 0.41; // a default weight for suv max often used in the literature
|
||||
const DEFAULT_STRATEGY = ROI_STAT;
|
||||
|
||||
function reducer(state, action) {
|
||||
const { payload } = action;
|
||||
const { strategy, lower, upper, weight } = payload;
|
||||
|
||||
switch (action.type) {
|
||||
case 'setStrategy':
|
||||
return {
|
||||
...state,
|
||||
strategy,
|
||||
};
|
||||
case 'setThreshold':
|
||||
return {
|
||||
...state,
|
||||
lower: lower ? lower : state.lower,
|
||||
upper: upper ? upper : state.upper,
|
||||
};
|
||||
case 'setWeight':
|
||||
return {
|
||||
...state,
|
||||
weight,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default function PanelRoiThresholdSegmentation({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
}) {
|
||||
const { SegmentationService } = servicesManager.services;
|
||||
|
||||
const { t } = useTranslation('PanelSUV');
|
||||
const [showConfig, setShowConfig] = useState(false);
|
||||
const [labelmapLoading, setLabelmapLoading] = useState(false);
|
||||
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
|
||||
const [segmentations, setSegmentations] = useState(() =>
|
||||
SegmentationService.getSegmentations()
|
||||
);
|
||||
|
||||
const [config, dispatch] = useReducer(reducer, {
|
||||
strategy: DEFAULT_STRATEGY,
|
||||
lower: LOWER_THRESHOLD_DEFAULT,
|
||||
upper: UPPER_THRESHOLD_DEFAULT,
|
||||
weight: WEIGHT_DEFAULT,
|
||||
});
|
||||
|
||||
const [tmtvValue, setTmtvValue] = useState(null);
|
||||
|
||||
const runCommand = useCallback(
|
||||
(commandName, commandOptions = {}) => {
|
||||
return commandsManager.runCommand(commandName, commandOptions);
|
||||
},
|
||||
[commandsManager]
|
||||
);
|
||||
|
||||
const handleTMTVCalculation = useCallback(() => {
|
||||
const tmtv = runCommand('calculateTMTV', { segmentations });
|
||||
|
||||
if (tmtv !== undefined) {
|
||||
setTmtvValue(tmtv.toFixed(2));
|
||||
}
|
||||
}, [segmentations, runCommand]);
|
||||
|
||||
const handleROIThresholding = useCallback(() => {
|
||||
const labelmap = runCommand('thresholdSegmentationByRectangleROITool', {
|
||||
segmentationId: selectedSegmentationId,
|
||||
config,
|
||||
});
|
||||
|
||||
const lesionStats = runCommand('getLesionStats', { labelmap });
|
||||
const suvPeak = runCommand('calculateSuvPeak', { labelmap });
|
||||
const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue;
|
||||
|
||||
// update segDetails with the suv peak for the active segmentation
|
||||
const segmentation = SegmentationService.getSegmentation(
|
||||
selectedSegmentationId
|
||||
);
|
||||
|
||||
const data = {
|
||||
lesionStats,
|
||||
suvPeak,
|
||||
lesionGlyoclysisStats,
|
||||
};
|
||||
|
||||
const notYetUpdatedAtSource = true;
|
||||
SegmentationService.addOrUpdateSegmentation(
|
||||
selectedSegmentationId,
|
||||
{
|
||||
...segmentation,
|
||||
data: Object.assign(segmentation.data, data),
|
||||
text: [`SUV Peak: ${suvPeak.suvPeak.toFixed(2)}`],
|
||||
},
|
||||
notYetUpdatedAtSource
|
||||
);
|
||||
|
||||
handleTMTVCalculation();
|
||||
}, [selectedSegmentationId, config]);
|
||||
|
||||
/**
|
||||
* Update UI based on segmentation changes (added, removed, updated)
|
||||
*/
|
||||
useEffect(() => {
|
||||
// ~~ Subscription
|
||||
const added = SegmentationService.EVENTS.SEGMENTATION_ADDED;
|
||||
const updated = SegmentationService.EVENTS.SEGMENTATION_UPDATED;
|
||||
const subscriptions = [];
|
||||
|
||||
[added, updated].forEach(evt => {
|
||||
const { unsubscribe } = SegmentationService.subscribe(evt, () => {
|
||||
const segmentations = SegmentationService.getSegmentations();
|
||||
setSegmentations(segmentations);
|
||||
});
|
||||
subscriptions.push(unsubscribe);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach(unsub => {
|
||||
unsub();
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = SegmentationService.subscribe(
|
||||
SegmentationService.EVENTS.SEGMENTATION_REMOVED,
|
||||
() => {
|
||||
const segmentations = SegmentationService.getSegmentations();
|
||||
setSegmentations(segmentations);
|
||||
|
||||
if (segmentations.length > 0) {
|
||||
setSelectedSegmentationId(segmentations[0].id);
|
||||
handleTMTVCalculation();
|
||||
} else {
|
||||
setSelectedSegmentationId(null);
|
||||
setTmtvValue(null);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Toggle visibility of the segmentation
|
||||
*/
|
||||
useEffect(() => {
|
||||
const subscription = SegmentationService.subscribe(
|
||||
SegmentationService.EVENTS.SEGMENTATION_VISIBILITY_CHANGED,
|
||||
({ segmentation }) => {
|
||||
runCommand('toggleSegmentationVisibility', {
|
||||
segmentationId: segmentation.id,
|
||||
});
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [SegmentationService]);
|
||||
|
||||
/**
|
||||
* Whenever the segmentations change, update the TMTV calculations
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!selectedSegmentationId && segmentations.length > 0) {
|
||||
setSelectedSegmentationId(segmentations[0].id);
|
||||
}
|
||||
|
||||
handleTMTVCalculation();
|
||||
}, [segmentations, selectedSegmentationId]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-between h-full">
|
||||
<div className="overflow-x-hidden overflow-y-auto invisible-scrollbar">
|
||||
<div className="flex mx-4 my-4 mb-4 space-x-4">
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setLabelmapLoading(true);
|
||||
setTimeout(() => {
|
||||
runCommand('createNewLabelmapFromPT').then(segmentationId => {
|
||||
setLabelmapLoading(false);
|
||||
setSelectedSegmentationId(segmentationId);
|
||||
});
|
||||
});
|
||||
}}
|
||||
className={classnames(
|
||||
'text-xs text-white border-b border-transparent'
|
||||
)}
|
||||
>
|
||||
{labelmapLoading ? 'loading ...' : 'New Label'}
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={handleROIThresholding}
|
||||
className="text-xs text-white border-b border-transparent "
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center justify-around h-8 mb-2 border-t outline-none cursor-pointer select-none bg-secondary-dark first:border-0 border-secondary-light"
|
||||
onClick={() => {
|
||||
setShowConfig(!showConfig);
|
||||
}}
|
||||
>
|
||||
<div className="px-4 text-base text-white">
|
||||
{t('ROI Threshold Configuration')}
|
||||
</div>
|
||||
</div>
|
||||
{showConfig && (
|
||||
<ROIThresholdConfiguration
|
||||
config={config}
|
||||
dispatch={dispatch}
|
||||
runCommand={runCommand}
|
||||
/>
|
||||
)}
|
||||
{/* show segmentation table */}
|
||||
<div className="mt-4">
|
||||
{segmentations?.length ? (
|
||||
<SegmentationTable
|
||||
title={t('Segmentations')}
|
||||
amount={segmentations.length}
|
||||
segmentations={segmentations}
|
||||
activeSegmentationId={selectedSegmentationId}
|
||||
onClick={id => {
|
||||
runCommand('setSegmentationActiveForToolGroups', {
|
||||
segmentationId: id,
|
||||
});
|
||||
setSelectedSegmentationId(id);
|
||||
}}
|
||||
onToggleVisibility={id => {
|
||||
SegmentationService.toggleSegmentationsVisibility([id]);
|
||||
}}
|
||||
onToggleVisibilityAll={ids => {
|
||||
SegmentationService.toggleSegmentationsVisibility(ids);
|
||||
}}
|
||||
onDelete={id => {
|
||||
SegmentationService.remove(id);
|
||||
}}
|
||||
onEdit={id => {
|
||||
segmentationEditHandler({
|
||||
id,
|
||||
servicesManager,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{tmtvValue !== null ? (
|
||||
<div className="flex items-baseline justify-between px-2 py-1 mt-4 bg-secondary-dark">
|
||||
<span className="text-base font-bold tracking-widest text-white uppercase">
|
||||
{'TMTV:'}
|
||||
</span>
|
||||
<div className="text-white">{`${tmtvValue} mL`}</div>
|
||||
</div>
|
||||
) : null}
|
||||
<ExportReports
|
||||
segmentations={segmentations}
|
||||
tmtvValue={tmtvValue}
|
||||
config={config}
|
||||
commandsManager={commandsManager}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="opacity-50 hover:opacity-80 flex items-center justify-center text-blue-400 mb-4 cursor-pointer"
|
||||
onClick={() => {
|
||||
// navigate to a url in a new tab
|
||||
window.open(
|
||||
'https://github.com/OHIF/Viewers/blob/feat/segmentation-service/modes/tmtv/README.md',
|
||||
'_blank'
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
width="15px"
|
||||
height="15px"
|
||||
name={'info'}
|
||||
className={'ml-4 mr-3 text-primary-active'}
|
||||
/>
|
||||
<span>{'About'}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
PanelRoiThresholdSegmentation.propTypes = {
|
||||
commandsManager: PropTypes.shape({
|
||||
runCommand: PropTypes.func.isRequired,
|
||||
}),
|
||||
servicesManager: PropTypes.shape({
|
||||
services: PropTypes.shape({
|
||||
SegmentationService: PropTypes.shape({
|
||||
getSegmentation: PropTypes.func.isRequired,
|
||||
getSegmentations: PropTypes.func.isRequired,
|
||||
toggleSegmentationsVisibility: PropTypes.func.isRequired,
|
||||
subscribe: PropTypes.func.isRequired,
|
||||
EVENTS: PropTypes.object.isRequired,
|
||||
VALUE_TYPES: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
import React from 'react';
|
||||
import { Input, Select, Button, ButtonGroup } from '@ohif/ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const ROI_STAT = 'roi_stat';
|
||||
const RANGE = 'range';
|
||||
|
||||
const options = [
|
||||
{ value: ROI_STAT, label: 'Max', placeHolder: 'Max' },
|
||||
{ value: RANGE, label: 'Range', placeHolder: 'Range' },
|
||||
];
|
||||
|
||||
function ROIThresholdConfiguration({ config, dispatch, runCommand }) {
|
||||
const { t } = useTranslation('ROIThresholdConfiguration');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col px-4 space-y-4 bg-primary-dark">
|
||||
<div className="flex items-end space-x-2">
|
||||
<div className="flex flex-col w-1/2 mt-2 ">
|
||||
<Select
|
||||
label={t('Strategy')}
|
||||
closeMenuOnSelect={true}
|
||||
className="mr-2 bg-black border-primary-main "
|
||||
options={options}
|
||||
placeholder={
|
||||
options.find(option => option.value === config.strategy)
|
||||
.placeHolder
|
||||
}
|
||||
value={config.strategy}
|
||||
onChange={({ value }) => {
|
||||
dispatch({
|
||||
type: 'setStrategy',
|
||||
payload: {
|
||||
strategy: value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<ButtonGroup color="black" size="inherit">
|
||||
<Button
|
||||
className="px-2 py-2 text-base"
|
||||
onClick={() => runCommand('setStartSliceForROIThresholdTool')}
|
||||
>
|
||||
{t('Start')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup color="black" size="inherit">
|
||||
<Button
|
||||
className="px-2 py-2 text-base"
|
||||
onClick={() => runCommand('setEndSliceForROIThresholdTool')}
|
||||
>
|
||||
{t('End')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{config.strategy === ROI_STAT && (
|
||||
<Input
|
||||
label={t('Percentage of Max SUV')}
|
||||
labelClassName="text-white"
|
||||
className="mt-2 bg-black border-primary-main"
|
||||
type="text"
|
||||
containerClassName="mr-2"
|
||||
value={config.weight}
|
||||
onChange={e => {
|
||||
dispatch({
|
||||
type: 'setWeight',
|
||||
payload: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{config.strategy !== ROI_STAT && (
|
||||
<div className="flex justify-between">
|
||||
<Input
|
||||
label={t('Lower')}
|
||||
labelClassName="text-white"
|
||||
className="mt-2 bg-black border-primary-main"
|
||||
type="text"
|
||||
containerClassName="mr-2"
|
||||
value={config.lower}
|
||||
onChange={e => {
|
||||
dispatch({
|
||||
type: 'setThreshold',
|
||||
payload: {
|
||||
lower: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
label={t('Upper')}
|
||||
labelClassName="text-white"
|
||||
className="mt-2 bg-black border-primary-main"
|
||||
type="text"
|
||||
containerClassName="mr-2"
|
||||
value={config.upper}
|
||||
onChange={e => {
|
||||
dispatch({
|
||||
type: 'setThreshold',
|
||||
payload: {
|
||||
upper: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ROIThresholdConfiguration;
|
||||
@@ -0,0 +1,3 @@
|
||||
import PanelROIThresholdSegmentation from './PanelROIThresholdSegmentation';
|
||||
|
||||
export default PanelROIThresholdSegmentation;
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { Input, Dialog } from '@ohif/ui';
|
||||
|
||||
function segmentationItemEditHandler({ id, servicesManager }) {
|
||||
const { SegmentationService, UIDialogService } = servicesManager.services;
|
||||
|
||||
const segmentation = SegmentationService.getSegmentation(id);
|
||||
|
||||
const onSubmitHandler = ({ action, value }) => {
|
||||
switch (action.id) {
|
||||
case 'save': {
|
||||
SegmentationService.addOrUpdateSegmentation(
|
||||
id,
|
||||
{
|
||||
...segmentation,
|
||||
...value,
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
UIDialogService.dismiss({ id: 'enter-annotation' });
|
||||
};
|
||||
|
||||
UIDialogService.create({
|
||||
id: 'enter-annotation',
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
showOverlay: true,
|
||||
content: Dialog,
|
||||
contentProps: {
|
||||
title: 'Enter your annotation',
|
||||
noCloseButton: true,
|
||||
value: { label: segmentation.label || '' },
|
||||
body: ({ value, setValue }) => {
|
||||
const onChangeHandler = event => {
|
||||
event.persist();
|
||||
setValue(value => ({ ...value, label: event.target.value }));
|
||||
};
|
||||
|
||||
const onKeyPressHandler = event => {
|
||||
if (event.key === 'Enter') {
|
||||
onSubmitHandler({ value, action: { id: 'save' } });
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="p-4 bg-primary-dark">
|
||||
<Input
|
||||
autoFocus
|
||||
className="mt-2 bg-black border-primary-main"
|
||||
type="text"
|
||||
containerClassName="mr-2"
|
||||
value={value.label}
|
||||
onChange={onChangeHandler}
|
||||
onKeyPress={onKeyPressHandler}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
actions: [
|
||||
// temp: swap button types until colors are updated
|
||||
{ id: 'cancel', text: 'Cancel', type: 'primary' },
|
||||
{ id: 'save', text: 'Save', type: 'secondary' },
|
||||
],
|
||||
onSubmit: onSubmitHandler,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default segmentationItemEditHandler;
|
||||
@@ -0,0 +1,4 @@
|
||||
import PanelPetSUV from './PanelPetSUV';
|
||||
import PanelROIThresholdSegmentation from './PanelROIThresholdSegmentation';
|
||||
|
||||
export { PanelPetSUV, PanelROIThresholdSegmentation };
|
||||
Reference in new issue
Block a user