feat(4D): Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode (#3664)
Co-authored-by: Alireza <ar.sedghi@gmail.com> Co-authored-by: Neil <neil.a.macphee@gmail.com>
This commit is contained in:
200 files changed
+9589
-2178
No files matched your search
@@ -1,51 +0,0 @@
|
||||
import React from 'react';
|
||||
import { LegacyButton, LegacyButtonGroup } from '@ohif/ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function ExportReports({ segmentations, tmtvValue, config, commandsManager }) {
|
||||
const { t } = useTranslation('PanelSUVExport');
|
||||
|
||||
return (
|
||||
<>
|
||||
{segmentations?.length ? (
|
||||
<div className="mt-4 flex justify-center space-x-2">
|
||||
{/* TODO Revisit design of LegacyButtonGroup later - for now use LegacyButton for its children.*/}
|
||||
<LegacyButtonGroup
|
||||
color="black"
|
||||
size="inherit"
|
||||
>
|
||||
<LegacyButton
|
||||
className="px-2 py-2 text-base"
|
||||
disabled={tmtvValue === null}
|
||||
onClick={() => {
|
||||
commandsManager.runCommand('exportTMTVReportCSV', {
|
||||
segmentations,
|
||||
tmtv: tmtvValue,
|
||||
config,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('Export CSV')}
|
||||
</LegacyButton>
|
||||
</LegacyButtonGroup>
|
||||
<LegacyButtonGroup
|
||||
color="black"
|
||||
size="inherit"
|
||||
>
|
||||
<LegacyButton
|
||||
className="px-2 py-2 text-base"
|
||||
onClick={() => {
|
||||
commandsManager.runCommand('createTMTVRTReport');
|
||||
}}
|
||||
disabled={tmtvValue === null}
|
||||
>
|
||||
{t('Create RT Report')}
|
||||
</LegacyButton>
|
||||
</LegacyButtonGroup>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExportReports;
|
||||
-287
@@ -1,287 +0,0 @@
|
||||
import React, { useEffect, useState, useCallback, useReducer } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SegmentationTable, Button, Icon } from '@ohif/ui';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import segmentationEditHandler from './segmentationEditHandler';
|
||||
import ExportReports from './ExportReports';
|
||||
import ROIThresholdConfiguration, { ROI_STAT } from './ROIThresholdConfiguration';
|
||||
|
||||
const LOWER_CT_THRESHOLD_DEFAULT = -1024;
|
||||
const UPPER_CT_THRESHOLD_DEFAULT = 1024;
|
||||
const LOWER_PT_THRESHOLD_DEFAULT = 2.5;
|
||||
const UPPER_PT_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, ctLower, ctUpper, ptLower, ptUpper, weight } = payload;
|
||||
|
||||
switch (action.type) {
|
||||
case 'setStrategy':
|
||||
return {
|
||||
...state,
|
||||
strategy,
|
||||
};
|
||||
case 'setThreshold':
|
||||
return {
|
||||
...state,
|
||||
ctLower: ctLower ? ctLower : state.ctLower,
|
||||
ctUpper: ctUpper ? ctUpper : state.ctUpper,
|
||||
ptLower: ptLower ? ptLower : state.ptLower,
|
||||
ptUpper: ptUpper ? ptUpper : state.ptUpper,
|
||||
};
|
||||
case 'setWeight':
|
||||
return {
|
||||
...state,
|
||||
weight,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default function LegacyPanelRoiThresholdSegmentation({ 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,
|
||||
ctLower: LOWER_CT_THRESHOLD_DEFAULT,
|
||||
ctUpper: UPPER_CT_THRESHOLD_DEFAULT,
|
||||
ptLower: LOWER_PT_THRESHOLD_DEFAULT,
|
||||
ptUpper: UPPER_PT_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 cachedStats = {
|
||||
lesionStats,
|
||||
suvPeak,
|
||||
lesionGlyoclysisStats,
|
||||
};
|
||||
|
||||
const notYetUpdatedAtSource = true;
|
||||
segmentationService.addOrUpdateSegmentation(
|
||||
{
|
||||
...segmentation,
|
||||
...Object.assign(segmentation.cachedStats, cachedStats),
|
||||
displayText: [`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();
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 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">
|
||||
<div className="invisible-scrollbar overflow-y-auto overflow-x-hidden">
|
||||
<div className="mx-4 my-4 mb-4 flex space-x-4">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setLabelmapLoading(true);
|
||||
setTimeout(() => {
|
||||
runCommand('createNewLabelmapFromPT').then(segmentationId => {
|
||||
setLabelmapLoading(false);
|
||||
setSelectedSegmentationId(segmentationId);
|
||||
});
|
||||
});
|
||||
}}
|
||||
>
|
||||
{labelmapLoading ? 'loading ...' : 'New Label'}
|
||||
</Button>
|
||||
<Button onClick={handleROIThresholding}>Run</Button>
|
||||
</div>
|
||||
<div
|
||||
className="bg-secondary-dark border-secondary-light mb-2 flex h-8 cursor-pointer select-none items-center justify-around border-t outline-none first:border-0"
|
||||
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')}
|
||||
segmentations={segmentations}
|
||||
activeSegmentationId={selectedSegmentationId}
|
||||
onClick={id => {
|
||||
runCommand('setSegmentationActiveForToolGroups', {
|
||||
segmentationId: id,
|
||||
});
|
||||
setSelectedSegmentationId(id);
|
||||
}}
|
||||
onToggleVisibility={id => {
|
||||
segmentationService.toggleSegmentationVisibility(id);
|
||||
}}
|
||||
onToggleVisibilityAll={ids => {
|
||||
ids.map(id => {
|
||||
segmentationService.toggleSegmentationVisibility(id);
|
||||
});
|
||||
}}
|
||||
onDelete={id => {
|
||||
segmentationService.remove(id);
|
||||
}}
|
||||
onEdit={id => {
|
||||
segmentationEditHandler({
|
||||
id,
|
||||
servicesManager,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{tmtvValue !== null ? (
|
||||
<div className="bg-secondary-dark mt-4 flex items-baseline justify-between px-2 py-1">
|
||||
<span className="text-base font-bold uppercase tracking-widest text-white">
|
||||
{'TMTV:'}
|
||||
</span>
|
||||
<div className="text-white">{`${tmtvValue} mL`}</div>
|
||||
</div>
|
||||
) : null}
|
||||
<ExportReports
|
||||
segmentations={segmentations}
|
||||
tmtvValue={tmtvValue}
|
||||
config={config}
|
||||
commandsManager={commandsManager}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="mt-auto mb-4 flex cursor-pointer items-center justify-center text-blue-400 opacity-50 hover:opacity-80"
|
||||
onClick={() => {
|
||||
// navigate to a url in a new tab
|
||||
window.open('https://github.com/OHIF/Viewers/blob/master/modes/tmtv/README.md', '_blank');
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
width="15px"
|
||||
height="15px"
|
||||
name={'info'}
|
||||
className={'text-primary-active ml-4 mr-3'}
|
||||
/>
|
||||
<span>{'User Guide'}</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
LegacyPanelRoiThresholdSegmentation.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,
|
||||
toggleSegmentationVisibility: PropTypes.func.isRequired,
|
||||
subscribe: PropTypes.func.isRequired,
|
||||
EVENTS: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icon, ActionButtons } from '@ohif/ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
export default function PanelRoiThresholdSegmentation({ servicesManager, commandsManager }) {
|
||||
const { segmentationService } = servicesManager.services;
|
||||
const { t } = useTranslation('PanelSUVExport');
|
||||
|
||||
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
|
||||
|
||||
/**
|
||||
* Update UI based on segmentation changes (added, removed, updated)
|
||||
*/
|
||||
useEffect(() => {
|
||||
// ~~ Subscription
|
||||
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
|
||||
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
|
||||
const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
|
||||
const subscriptions = [];
|
||||
|
||||
[added, updated, removed].forEach(evt => {
|
||||
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
setSegmentations(segmentations);
|
||||
});
|
||||
subscriptions.push(unsubscribe);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach(unsub => {
|
||||
unsub();
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const tmtvValue = segmentations?.[0]?.cachedStats?.tmtv?.value || null;
|
||||
const config = segmentations?.[0]?.cachedStats?.tmtv?.config || {};
|
||||
|
||||
const actions = [
|
||||
{
|
||||
label: 'Export CSV',
|
||||
onClick: () => {
|
||||
commandsManager.runCommand('exportTMTVReportCSV', {
|
||||
segmentations,
|
||||
tmtv: tmtvValue,
|
||||
config,
|
||||
});
|
||||
},
|
||||
disabled: tmtvValue === null,
|
||||
},
|
||||
{
|
||||
label: 'Create RT Report',
|
||||
onClick: () => {
|
||||
commandsManager.runCommand('createTMTVRTReport');
|
||||
},
|
||||
disabled: tmtvValue === null,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-1 flex flex-col">
|
||||
<div className="invisible-scrollbar overflow-y-auto overflow-x-hidden">
|
||||
{tmtvValue !== null ? (
|
||||
<div className="bg-secondary-dark mt-1 flex items-baseline justify-between px-2 py-1">
|
||||
<span className="text-base font-bold uppercase tracking-widest text-white">
|
||||
{'TMTV:'}
|
||||
</span>
|
||||
<div className="text-white">{`${tmtvValue} mL`}</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-1 flex justify-center">
|
||||
<ActionButtons
|
||||
actions={actions}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="absolute bottom-1 flex cursor-pointer items-center justify-center text-blue-400 opacity-50 hover:opacity-80"
|
||||
onClick={() => {
|
||||
// navigate to a url in a new tab
|
||||
window.open('https://github.com/OHIF/Viewers/blob/master/modes/tmtv/README.md', '_blank');
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
width="15px"
|
||||
height="15px"
|
||||
name={'info'}
|
||||
className={'text-primary-active ml-4 mr-3'}
|
||||
/>
|
||||
<span>{'User Guide'}</span>
|
||||
</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,
|
||||
toggleSegmentationVisibility: PropTypes.func.isRequired,
|
||||
subscribe: PropTypes.func.isRequired,
|
||||
EVENTS: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
-317
@@ -1,317 +0,0 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SegmentationGroupTableExpanded, Icon } from '@ohif/ui';
|
||||
import { createReportAsync } from '@ohif/extension-default';
|
||||
|
||||
import segmentationEditHandler from './segmentationEditHandler';
|
||||
import ExportReports from './ExportReports';
|
||||
import callInputDialog from './callInputDialog';
|
||||
import callColorPickerDialog from './colorPickerDialog';
|
||||
|
||||
export default function PanelRoiThresholdSegmentation({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
}) {
|
||||
const { segmentationService, viewportGridService, uiDialogService } = servicesManager.services;
|
||||
|
||||
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
|
||||
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
|
||||
|
||||
const runCommand = useCallback(
|
||||
(commandName, commandOptions = {}) => {
|
||||
return commandsManager.runCommand(commandName, commandOptions);
|
||||
},
|
||||
[commandsManager]
|
||||
);
|
||||
|
||||
/**
|
||||
* Update UI based on segmentation changes (added, removed, updated)
|
||||
*/
|
||||
useEffect(() => {
|
||||
// ~~ Subscription
|
||||
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
|
||||
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
|
||||
const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
|
||||
const subscriptions = [];
|
||||
|
||||
[added, updated, removed].forEach(evt => {
|
||||
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
setSegmentations(segmentations);
|
||||
});
|
||||
subscriptions.push(unsubscribe);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach(unsub => {
|
||||
unsub();
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onSegmentationClick = (segmentationId: string) => {
|
||||
segmentationService.setActiveSegmentationForToolGroup(segmentationId);
|
||||
setSelectedSegmentationId(segmentationId);
|
||||
};
|
||||
|
||||
const onSegmentationAdd = async () => {
|
||||
runCommand('createNewLabelmapFromPT').then(segmentationId => {
|
||||
setSelectedSegmentationId(segmentationId);
|
||||
});
|
||||
};
|
||||
|
||||
const onSegmentAdd = segmentationId => {
|
||||
segmentationService.addSegment(segmentationId);
|
||||
};
|
||||
|
||||
const getToolGroupIds = segmentationId => {
|
||||
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId);
|
||||
|
||||
return toolGroupIds;
|
||||
};
|
||||
|
||||
const onSegmentClick = (segmentationId, segmentIndex) => {
|
||||
segmentationService.setActiveSegment(segmentationId, segmentIndex);
|
||||
|
||||
const toolGroupIds = getToolGroupIds(segmentationId);
|
||||
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
// const toolGroupId =
|
||||
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
|
||||
segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex, toolGroupId);
|
||||
});
|
||||
};
|
||||
|
||||
const _setSegmentationConfiguration = useCallback(
|
||||
(segmentationId, key, value) => {
|
||||
segmentationService.setConfiguration({
|
||||
segmentationId,
|
||||
[key]: value,
|
||||
});
|
||||
},
|
||||
[segmentationService]
|
||||
);
|
||||
|
||||
const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
const segmentInfo = segmentation.segments[segmentIndex];
|
||||
const isVisible = !segmentInfo.isVisible;
|
||||
const toolGroupIds = getToolGroupIds(segmentationId);
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
segmentationService.setSegmentVisibility(
|
||||
segmentationId,
|
||||
segmentIndex,
|
||||
isVisible,
|
||||
toolGroupId
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const onSegmentDelete = (segmentationId, segmentIndex) => {
|
||||
segmentationService.removeSegment(segmentationId, segmentIndex);
|
||||
};
|
||||
|
||||
const onSegmentEdit = (segmentationId, segmentIndex) => {
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
const segment = segmentation.segments[segmentIndex];
|
||||
const { label } = segment;
|
||||
|
||||
callInputDialog(uiDialogService, label, (label, actionId) => {
|
||||
if (label === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
segmentationService.setSegmentLabel(segmentationId, segmentIndex, label);
|
||||
});
|
||||
};
|
||||
|
||||
const onToggleSegmentLock = (segmentationId, segmentIndex) => {
|
||||
segmentationService.toggleSegmentLocked(segmentationId, segmentIndex);
|
||||
};
|
||||
|
||||
const onSegmentColorClick = (segmentationId, segmentIndex) => {
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
const segment = segmentation.segments[segmentIndex];
|
||||
const { color, opacity } = segment;
|
||||
|
||||
const rgbaColor = {
|
||||
r: color[0],
|
||||
g: color[1],
|
||||
b: color[2],
|
||||
a: opacity / 255.0,
|
||||
};
|
||||
|
||||
callColorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
|
||||
if (actionId === 'cancel') {
|
||||
return;
|
||||
}
|
||||
|
||||
segmentationService.setSegmentRGBAColor(segmentationId, segmentIndex, [
|
||||
newRgbaColor.r,
|
||||
newRgbaColor.g,
|
||||
newRgbaColor.b,
|
||||
newRgbaColor.a * 255.0,
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
const storeSegmentation = async segmentationId => {
|
||||
const datasources = extensionManager.getActiveDataSource();
|
||||
|
||||
const displaySetInstanceUIDs = await createReportAsync({
|
||||
servicesManager,
|
||||
getReport: () =>
|
||||
commandsManager.runCommand('storeSegmentation', {
|
||||
segmentationId,
|
||||
dataSource: datasources[0],
|
||||
}),
|
||||
reportType: 'Segmentation',
|
||||
});
|
||||
|
||||
// Show the exported report in the active viewport as read only (similar to SR)
|
||||
if (displaySetInstanceUIDs) {
|
||||
// clear the segmentation that we exported, similar to the storeMeasurement
|
||||
// where we remove the measurements and prompt again the user if they would like
|
||||
// to re-read the measurements in a SR read only viewport
|
||||
segmentationService.remove(segmentationId);
|
||||
|
||||
viewportGridService.setDisplaySetsForViewport({
|
||||
viewportId: viewportGridService.getActiveViewportId(),
|
||||
displaySetInstanceUIDs,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onSegmentationDownloadRTSS = segmentationId => {
|
||||
commandsManager.runCommand('downloadRTSS', {
|
||||
segmentationId,
|
||||
});
|
||||
};
|
||||
|
||||
const onSegmentationDownload = segmentationId => {
|
||||
commandsManager.runCommand('downloadSegmentation', {
|
||||
segmentationId,
|
||||
});
|
||||
};
|
||||
|
||||
const tmtvValue = segmentations?.[0]?.cachedStats?.tmtv?.value || null;
|
||||
const config = segmentations?.[0]?.cachedStats?.tmtv?.config || {};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
<div className="invisible-scrollbar overflow-y-auto overflow-x-hidden">
|
||||
{/* show segmentation table */}
|
||||
<div className="flex min-h-0 flex-col bg-black text-[13px] font-[300]">
|
||||
<SegmentationGroupTableExpanded
|
||||
disableEditing={false}
|
||||
showAddSegmentation={true}
|
||||
showAddSegment={false}
|
||||
showDeleteSegment={true}
|
||||
segmentations={segmentations}
|
||||
onSegmentationAdd={onSegmentationAdd}
|
||||
onSegmentationClick={onSegmentationClick}
|
||||
onToggleSegmentationVisibility={id => {
|
||||
segmentationService.toggleSegmentationVisibility(id);
|
||||
}}
|
||||
onToggleSegmentVisibility={onToggleSegmentVisibility}
|
||||
onSegmentationDelete={id => {
|
||||
segmentationService.remove(id);
|
||||
}}
|
||||
onSegmentationEdit={id => {
|
||||
segmentationEditHandler({
|
||||
id,
|
||||
servicesManager,
|
||||
});
|
||||
}}
|
||||
segmentationConfig={{ initialConfig: segmentationService.getConfiguration() }}
|
||||
onSegmentAdd={onSegmentAdd}
|
||||
onSegmentClick={onSegmentClick}
|
||||
onSegmentDelete={onSegmentDelete}
|
||||
onSegmentEdit={onSegmentEdit}
|
||||
onToggleSegmentLock={onToggleSegmentLock}
|
||||
onSegmentColorClick={onSegmentColorClick}
|
||||
storeSegmentation={storeSegmentation}
|
||||
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
|
||||
onSegmentationDownload={onSegmentationDownload}
|
||||
setRenderOutline={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value)
|
||||
}
|
||||
setOutlineOpacityActive={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value)
|
||||
}
|
||||
setRenderFill={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
|
||||
}
|
||||
setRenderInactiveSegmentations={value =>
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'renderInactiveSegmentations',
|
||||
value
|
||||
)
|
||||
}
|
||||
setOutlineWidthActive={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
|
||||
}
|
||||
setFillAlpha={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value)
|
||||
}
|
||||
setFillAlphaInactive={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{tmtvValue !== null ? (
|
||||
<div className="bg-secondary-dark mt-1 flex items-baseline justify-between px-2 py-1">
|
||||
<span className="text-base font-bold uppercase tracking-widest text-white">
|
||||
{'TMTV:'}
|
||||
</span>
|
||||
<div className="text-white">{`${tmtvValue} mL`}</div>
|
||||
</div>
|
||||
) : null}
|
||||
<ExportReports
|
||||
segmentations={segmentations}
|
||||
tmtvValue={tmtvValue}
|
||||
config={config}
|
||||
commandsManager={commandsManager}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="absolute bottom-1 flex cursor-pointer items-center justify-center text-blue-400 opacity-50 hover:opacity-80"
|
||||
onClick={() => {
|
||||
// navigate to a url in a new tab
|
||||
window.open('https://github.com/OHIF/Viewers/blob/master/modes/tmtv/README.md', '_blank');
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
width="15px"
|
||||
height="15px"
|
||||
name={'info'}
|
||||
className={'text-primary-active ml-4 mr-3'}
|
||||
/>
|
||||
<span>{'User Guide'}</span>
|
||||
</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,
|
||||
toggleSegmentationVisibility: PropTypes.func.isRequired,
|
||||
subscribe: PropTypes.func.isRequired,
|
||||
EVENTS: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Input, Dialog, ButtonEnums } from '@ohif/ui';
|
||||
|
||||
function callInputDialog(uiDialogService, label, callback) {
|
||||
const dialogId = 'enter-segment-label';
|
||||
|
||||
const onSubmitHandler = ({ action, value }) => {
|
||||
switch (action.id) {
|
||||
case 'save':
|
||||
callback(value.label, action.id);
|
||||
break;
|
||||
case 'cancel':
|
||||
callback('', action.id);
|
||||
break;
|
||||
}
|
||||
uiDialogService.dismiss({ id: dialogId });
|
||||
};
|
||||
|
||||
if (uiDialogService) {
|
||||
uiDialogService.create({
|
||||
id: dialogId,
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
showOverlay: true,
|
||||
content: Dialog,
|
||||
contentProps: {
|
||||
title: 'Segment',
|
||||
value: { label },
|
||||
noCloseButton: true,
|
||||
onClose: () => uiDialogService.dismiss({ id: dialogId }),
|
||||
actions: [
|
||||
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
|
||||
{ id: 'save', text: 'Confirm', type: ButtonEnums.type.primary },
|
||||
],
|
||||
onSubmit: onSubmitHandler,
|
||||
body: ({ value, setValue }) => {
|
||||
return (
|
||||
<Input
|
||||
label="Enter the segment label"
|
||||
labelClassName="text-white text-[14px] leading-[1.2]"
|
||||
autoFocus
|
||||
className="border-primary-main bg-black"
|
||||
type="text"
|
||||
value={value.label}
|
||||
onChange={event => {
|
||||
event.persist();
|
||||
setValue(value => ({ ...value, label: event.target.value }));
|
||||
}}
|
||||
onKeyPress={event => {
|
||||
if (event.key === 'Enter') {
|
||||
onSubmitHandler({ value, action: { id: 'save' } });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default callInputDialog;
|
||||
@@ -1,3 +0,0 @@
|
||||
.chrome-picker {
|
||||
background: #090c29 !important;
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Dialog } from '@ohif/ui';
|
||||
import { ChromePicker } from 'react-color';
|
||||
|
||||
import './colorPickerDialog.css';
|
||||
|
||||
function callColorPickerDialog(uiDialogService, rgbaColor, callback) {
|
||||
const dialogId = 'pick-color';
|
||||
|
||||
const onSubmitHandler = ({ action, value }) => {
|
||||
switch (action.id) {
|
||||
case 'save':
|
||||
callback(value.rgbaColor, action.id);
|
||||
break;
|
||||
case 'cancel':
|
||||
callback('', action.id);
|
||||
break;
|
||||
}
|
||||
uiDialogService.dismiss({ id: dialogId });
|
||||
};
|
||||
|
||||
if (uiDialogService) {
|
||||
uiDialogService.create({
|
||||
id: dialogId,
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
showOverlay: true,
|
||||
content: Dialog,
|
||||
contentProps: {
|
||||
title: 'Segment Color',
|
||||
value: { rgbaColor },
|
||||
noCloseButton: true,
|
||||
onClose: () => uiDialogService.dismiss({ id: dialogId }),
|
||||
actions: [
|
||||
{ id: 'cancel', text: 'Cancel', type: 'primary' },
|
||||
{ id: 'save', text: 'Save', type: 'secondary' },
|
||||
],
|
||||
onSubmit: onSubmitHandler,
|
||||
body: ({ value, setValue }) => {
|
||||
const handleChange = color => {
|
||||
setValue({ rgbaColor: color.rgb });
|
||||
};
|
||||
|
||||
return (
|
||||
<ChromePicker
|
||||
color={value.rgbaColor}
|
||||
onChange={handleChange}
|
||||
presetColors={[]}
|
||||
width={300}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default callColorPickerDialog;
|
||||
@@ -1,3 +1,3 @@
|
||||
import PanelROIThresholdSegmentation from './PanelROIThresholdSegmentation';
|
||||
import PanelROIThresholdExport from './PanelROIThresholdExport';
|
||||
|
||||
export default PanelROIThresholdSegmentation;
|
||||
export default PanelROIThresholdExport;
|
||||
@@ -1,4 +1,4 @@
|
||||
import PanelPetSUV from './PanelPetSUV';
|
||||
import PanelROIThresholdSegmentation from './PanelROIThresholdSegmentation';
|
||||
import PanelROIThresholdExport from './PanelROIThresholdSegmentation';
|
||||
|
||||
export { PanelPetSUV, PanelROIThresholdSegmentation };
|
||||
export { PanelPetSUV, PanelROIThresholdExport };
|
||||
@@ -11,7 +11,10 @@ import createAndDownloadTMTVReport from './utils/createAndDownloadTMTVReport';
|
||||
import dicomRTAnnotationExport from './utils/dicomRTAnnotationExport/RTStructureSet';
|
||||
|
||||
const metadataProvider = classes.MetadataProvider;
|
||||
const RECTANGLE_ROI_THRESHOLD_MANUAL = 'RectangleROIStartEndThreshold';
|
||||
const RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS = [
|
||||
'RectangleROIStartEndThreshold',
|
||||
'RectangleROIThreshold',
|
||||
];
|
||||
const LABELMAP = csTools.Enums.SegmentationRepresentations.Labelmap;
|
||||
|
||||
const commandsModule = ({ servicesManager, commandsManager, extensionManager }) => {
|
||||
@@ -52,6 +55,15 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
||||
return toolGroupIds;
|
||||
}
|
||||
|
||||
function _getAnnotationsSelectedByToolNames(toolNames) {
|
||||
return toolNames.reduce((allAnnotationUIDs, toolName) => {
|
||||
const annotationUIDs =
|
||||
csTools.annotation.selection.getAnnotationsSelectedByToolName(toolName);
|
||||
|
||||
return allAnnotationUIDs.concat(annotationUIDs);
|
||||
}, []);
|
||||
}
|
||||
|
||||
const actions = {
|
||||
getMatchingPTDisplaySet: ({ viewportMatchDetails }) => {
|
||||
// Todo: this is assuming that the hanging protocol has successfully matched
|
||||
@@ -108,7 +120,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
||||
|
||||
return metadata;
|
||||
},
|
||||
createNewLabelmapFromPT: async () => {
|
||||
createNewLabelmapFromPT: async ({ label }) => {
|
||||
// Create a segmentation of the same resolution as the source data
|
||||
// using volumeLoader.createAndCacheDerivedVolume.
|
||||
const { viewportMatchDetails } = hangingProtocolService.getMatchDetails();
|
||||
@@ -173,20 +185,8 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
||||
const { volumeId: segVolumeId } = representationData[LABELMAP];
|
||||
const { referencedVolumeId } = cs.cache.getVolume(segVolumeId);
|
||||
|
||||
const labelmapVolume = cs.cache.getVolume(segmentationId);
|
||||
const referencedVolume = cs.cache.getVolume(referencedVolumeId);
|
||||
const ctReferencedVolume = cs.cache.getVolume(ctVolumeId);
|
||||
|
||||
if (!referencedVolume) {
|
||||
throw new Error('No Reference volume found');
|
||||
}
|
||||
|
||||
if (!labelmapVolume) {
|
||||
throw new Error('No Reference labelmap found');
|
||||
}
|
||||
|
||||
const annotationUIDs = csTools.annotation.selection.getAnnotationsSelectedByToolName(
|
||||
RECTANGLE_ROI_THRESHOLD_MANUAL
|
||||
const annotationUIDs = _getAnnotationsSelectedByToolNames(
|
||||
RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS
|
||||
);
|
||||
|
||||
if (annotationUIDs.length === 0) {
|
||||
@@ -198,6 +198,57 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
||||
return;
|
||||
}
|
||||
|
||||
const labelmapVolume = cs.cache.getVolume(segmentationId);
|
||||
let referencedVolume = cs.cache.getVolume(referencedVolumeId);
|
||||
const ctReferencedVolume = cs.cache.getVolume(ctVolumeId);
|
||||
|
||||
// check if viewport is
|
||||
|
||||
if (!referencedVolume) {
|
||||
throw new Error('No Reference volume found');
|
||||
}
|
||||
|
||||
if (!labelmapVolume) {
|
||||
throw new Error('No Reference labelmap found');
|
||||
}
|
||||
|
||||
const annotation = csTools.annotation.state.getAnnotation(annotationUIDs[0]);
|
||||
|
||||
const {
|
||||
metadata: {
|
||||
enabledElement: { viewport },
|
||||
},
|
||||
} = annotation;
|
||||
|
||||
const showingReferenceVolume = viewport.hasVolumeId(referencedVolumeId);
|
||||
|
||||
if (!showingReferenceVolume) {
|
||||
// if the reference volume is not being displayed, we can't
|
||||
// rely on it for thresholding, we have couple of options here
|
||||
// 1. We choose whatever volume is being displayed
|
||||
// 2. We check if it is a fusion viewport, we pick the volume
|
||||
// that matches the size and dimensions of the labelmap. This might
|
||||
// happen if the 4D PT is converted to a computed volume and displayed
|
||||
// and wants to threshold the labelmap
|
||||
// 3. We throw an error
|
||||
const displaySetInstanceUIDs = viewportGridService.getDisplaySetsUIDsForViewport(
|
||||
viewport.id
|
||||
);
|
||||
|
||||
displaySetInstanceUIDs.forEach(displaySetInstanceUID => {
|
||||
const volume = cs.cache
|
||||
.getVolumes()
|
||||
.find(volume => volume.volumeId.includes(displaySetInstanceUID));
|
||||
|
||||
if (
|
||||
cs.utilities.isEqual(volume.dimensions, labelmapVolume.dimensions) &&
|
||||
cs.utilities.isEqual(volume.spacing, labelmapVolume.spacing)
|
||||
) {
|
||||
referencedVolume = volume;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const { ptLower, ptUpper, ctLower, ctUpper } = getThresholdValues(
|
||||
annotationUIDs,
|
||||
[referencedVolume, ctReferencedVolume],
|
||||
@@ -218,8 +269,8 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
||||
const { referencedVolumeId } = labelmap;
|
||||
const referencedVolume = cs.cache.getVolume(referencedVolumeId);
|
||||
|
||||
const annotationUIDs = csTools.annotation.selection.getAnnotationsSelectedByToolName(
|
||||
RECTANGLE_ROI_THRESHOLD_MANUAL
|
||||
const annotationUIDs = _getAnnotationsSelectedByToolNames(
|
||||
RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS
|
||||
);
|
||||
|
||||
const annotations = annotationUIDs.map(annotationUID =>
|
||||
@@ -236,8 +287,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
||||
},
|
||||
getLesionStats: ({ labelmap, segmentIndex = 1 }) => {
|
||||
const { scalarData, spacing } = labelmap;
|
||||
|
||||
const { scalarData: referencedScalarData } = cs.cache.getVolume(labelmap.referencedVolumeId);
|
||||
const referencedScalarData = cs.cache.getVolume(labelmap.referencedVolumeId).getScalarData();
|
||||
|
||||
let segmentationMax = -Infinity;
|
||||
let segmentationMin = Infinity;
|
||||
@@ -287,19 +337,25 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
||||
|
||||
return calculateTMTV(labelmaps);
|
||||
},
|
||||
exportTMTVReportCSV: ({ segmentations, tmtv, config }) => {
|
||||
exportTMTVReportCSV: ({ segmentations, tmtv, config, options }) => {
|
||||
const segReport = commandsManager.runCommand('getSegmentationCSVReport', {
|
||||
segmentations,
|
||||
});
|
||||
|
||||
const tlg = actions.getTotalLesionGlycolysis({ segmentations });
|
||||
const additionalReportRows = [
|
||||
{ key: 'Total Metabolic Tumor Volume', value: { tmtv } },
|
||||
{ key: 'Total Lesion Glycolysis', value: { tlg: tlg.toFixed(4) } },
|
||||
{ key: 'Threshold Configuration', value: { ...config } },
|
||||
];
|
||||
|
||||
createAndDownloadTMTVReport(segReport, additionalReportRows);
|
||||
if (tmtv !== undefined) {
|
||||
additionalReportRows.unshift({
|
||||
key: 'Total Metabolic Tumor Volume',
|
||||
value: { tmtv },
|
||||
});
|
||||
}
|
||||
|
||||
createAndDownloadTMTVReport(segReport, additionalReportRows, options);
|
||||
},
|
||||
getTotalLesionGlycolysis: ({ segmentations }) => {
|
||||
const labelmapVolumes = segmentations.map(s => segmentationService.getLabelmapVolume(s.id));
|
||||
@@ -323,9 +379,9 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
||||
}
|
||||
|
||||
const ptVolume = cs.cache.getVolume(referencedVolumeId);
|
||||
const mergedLabelData = mergedLabelmap.scalarData;
|
||||
const mergedLabelData = mergedLabelmap.getScalarData();
|
||||
|
||||
if (mergedLabelData.length !== ptVolume.scalarData.length) {
|
||||
if (mergedLabelData.length !== ptVolume.getScalarData().length) {
|
||||
console.error(
|
||||
'commandsModule::getTotalLesionGlycolysis:Labelmap and ptVolume are not the same size'
|
||||
);
|
||||
@@ -336,7 +392,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
||||
for (let i = 0; i < mergedLabelData.length; i++) {
|
||||
// if not background
|
||||
if (mergedLabelData[i] !== 0) {
|
||||
suv += ptVolume.scalarData[i];
|
||||
suv += ptVolume.getScalarData()[i];
|
||||
totalLesionVoxelCount += 1;
|
||||
}
|
||||
}
|
||||
@@ -351,8 +407,8 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
||||
const { viewport } = _getActiveViewportsEnabledElement();
|
||||
const { focalPoint, viewPlaneNormal } = viewport.getCamera();
|
||||
|
||||
const selectedAnnotationUIDs = csTools.annotation.selection.getAnnotationsSelectedByToolName(
|
||||
RECTANGLE_ROI_THRESHOLD_MANUAL
|
||||
const selectedAnnotationUIDs = _getAnnotationsSelectedByToolNames(
|
||||
RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS
|
||||
);
|
||||
|
||||
const annotationUID = selectedAnnotationUIDs[0];
|
||||
@@ -389,8 +445,8 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
||||
setEndSliceForROIThresholdTool: () => {
|
||||
const { viewport } = _getActiveViewportsEnabledElement();
|
||||
|
||||
const selectedAnnotationUIDs = csTools.annotation.selection.getAnnotationsSelectedByToolName(
|
||||
RECTANGLE_ROI_THRESHOLD_MANUAL
|
||||
const selectedAnnotationUIDs = _getAnnotationsSelectedByToolNames(
|
||||
RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS
|
||||
);
|
||||
|
||||
const annotationUID = selectedAnnotationUIDs[0];
|
||||
@@ -415,7 +471,11 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
||||
|
||||
Object.keys(stateManager.annotations).forEach(frameOfReferenceUID => {
|
||||
const forAnnotations = stateManager.annotations[frameOfReferenceUID];
|
||||
const ROIAnnotations = forAnnotations[RECTANGLE_ROI_THRESHOLD_MANUAL];
|
||||
const ROIAnnotations = RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS.reduce(
|
||||
(annotations, toolName) => [...annotations, ...(forAnnotations[toolName] ?? [])],
|
||||
[]
|
||||
);
|
||||
|
||||
annotations.push(...ROIAnnotations);
|
||||
});
|
||||
|
||||
@@ -483,7 +543,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
||||
|
||||
report[id] = {
|
||||
...segReport,
|
||||
PatientID: instance.PatientID,
|
||||
PatientID: instance.PatientID ?? '000000',
|
||||
PatientName: instance.PatientName.Alphabetic,
|
||||
StudyInstanceUID: instance.StudyInstanceUID,
|
||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { PanelPetSUV, PanelROIThresholdSegmentation } from './Panels';
|
||||
import { PanelPetSUV, PanelROIThresholdExport } from './Panels';
|
||||
import { Toolbox } from '@ohif/ui';
|
||||
|
||||
// TODO:
|
||||
@@ -17,20 +17,26 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedROIThresholdSeg = () => {
|
||||
const wrappedROIThresholdToolbox = () => {
|
||||
return (
|
||||
<>
|
||||
<Toolbox
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
buttonSectionId="tmtvToolbox"
|
||||
buttonSectionId="ROIThresholdToolbox"
|
||||
title="Threshold Tools"
|
||||
/>
|
||||
<PanelROIThresholdSegmentation
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedROIThresholdExport = () => {
|
||||
return (
|
||||
<>
|
||||
<PanelROIThresholdExport
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -45,11 +51,18 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
||||
component: wrappedPanelPetSuv,
|
||||
},
|
||||
{
|
||||
name: 'ROIThresholdSeg',
|
||||
name: 'tmtvBox',
|
||||
iconName: 'tab-segmentation',
|
||||
iconLabel: 'Segmentation',
|
||||
label: 'Segmentation',
|
||||
component: wrappedROIThresholdSeg,
|
||||
label: 'Segmentation Toolbox',
|
||||
component: wrappedROIThresholdToolbox,
|
||||
},
|
||||
{
|
||||
name: 'tmtvExport',
|
||||
iconName: 'tab-segmentation',
|
||||
iconLabel: 'Segmentation',
|
||||
label: 'Segmentation Export',
|
||||
component: wrappedROIThresholdExport,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -40,17 +40,19 @@ function calculateSuvPeak(
|
||||
return;
|
||||
}
|
||||
|
||||
if (labelmap.scalarData.length !== referenceVolume.scalarData.length) {
|
||||
const labelmapData = labelmap.getScalarData();
|
||||
const referenceVolumeData = referenceVolume.getScalarData();
|
||||
|
||||
if (labelmapData.length !== referenceVolumeData.length) {
|
||||
throw new Error('labelmap and referenceVolume must have the same number of pixels');
|
||||
}
|
||||
|
||||
const { scalarData: labelmapData, dimensions, imageData: labelmapImageData } = labelmap;
|
||||
|
||||
const { scalarData: referenceVolumeData, imageData: referenceVolumeImageData } = referenceVolume;
|
||||
const { dimensions, imageData: labelmapImageData } = labelmap;
|
||||
const { imageData: referenceVolumeImageData } = referenceVolume;
|
||||
|
||||
let boundsIJK;
|
||||
// Todo: using the first annotation for now
|
||||
if (annotations && annotations[0].data?.cachedStats) {
|
||||
if (annotations?.length && annotations[0].data?.cachedStats) {
|
||||
const { projectionPoints } = annotations[0].data.cachedStats;
|
||||
const pointsToUse = [].concat(...projectionPoints); // cannot use flat() because of typescript compiler right now
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default function createAndDownloadTMTVReport(segReport, additionalReportRows) {
|
||||
export default function createAndDownloadTMTVReport(segReport, additionalReportRows, options = {}) {
|
||||
const firstReport = segReport[Object.keys(segReport)[0]];
|
||||
const columns = Object.keys(firstReport);
|
||||
const csv = [columns.join(',')];
|
||||
@@ -40,6 +40,6 @@ export default function createAndDownloadTMTVReport(segReport, additionalReportR
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${firstReport.PatientID}_tmtv.csv`;
|
||||
a.download = options.filename ?? `${firstReport.PatientID}_tmtv.csv`;
|
||||
a.click();
|
||||
}
|
||||
Reference in new issue
Block a user