OHIF with Cornerstone3D 2.0 (#4347)
This commit is contained in:
1 parent
8981a61b8c
commit
7ce7127d7c
803 files changed
+32760
-15893
No files matched your search
+45
-140
@@ -1,171 +1,76 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icon, ActionButtons } from '@ohif/ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { eventTarget } from '@cornerstonejs/core';
|
||||
import { Enums } from '@cornerstonejs/tools';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useActiveViewportSegmentationRepresentations } from '@ohif/extension-cornerstone';
|
||||
import { handleROIThresholding } from '../../utils/handleROIThresholding';
|
||||
import { debounce } from '@ohif/core/src/utils';
|
||||
|
||||
export default function PanelRoiThresholdSegmentation({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
}: withAppTypes) {
|
||||
const { segmentationService, uiNotificationService } = servicesManager.services;
|
||||
const { t } = useTranslation('PanelSUVExport');
|
||||
const { segmentationService } = servicesManager.services;
|
||||
const { segmentationsWithRepresentations: segmentationsInfo } =
|
||||
useActiveViewportSegmentationRepresentations({ servicesManager });
|
||||
|
||||
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
|
||||
const [activeSegmentation, setActiveSegmentation] = useState(null);
|
||||
|
||||
/**
|
||||
* 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 = [];
|
||||
const segmentationIds = segmentationsInfo.map(
|
||||
segmentationInfo => segmentationInfo.segmentation.segmentationId
|
||||
);
|
||||
|
||||
[added, updated, removed].forEach(evt => {
|
||||
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
setSegmentations(segmentations);
|
||||
|
||||
const activeSegmentation = segmentations.filter(seg => seg.isActive);
|
||||
setActiveSegmentation(activeSegmentation[0]);
|
||||
});
|
||||
|
||||
subscriptions.push(unsubscribe);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach(unsub => {
|
||||
unsub();
|
||||
});
|
||||
const initialRun = async () => {
|
||||
for (const segmentationId of segmentationIds) {
|
||||
await handleROIThresholding({
|
||||
segmentationId,
|
||||
commandsManager,
|
||||
segmentationService,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
initialRun();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const callback = async evt => {
|
||||
const { detail } = evt;
|
||||
const { segmentationId } = detail;
|
||||
|
||||
if (!segmentationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const debouncedHandleROIThresholding = debounce(async eventDetail => {
|
||||
const { segmentationId } = eventDetail;
|
||||
await handleROIThresholding({
|
||||
segmentationId,
|
||||
commandsManager,
|
||||
segmentationService,
|
||||
});
|
||||
}, 100);
|
||||
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
const { cachedStats } = segmentation;
|
||||
if (!cachedStats) {
|
||||
return;
|
||||
}
|
||||
|
||||
// segment 1
|
||||
const suvPeak = cachedStats?.['1']?.suvPeak?.suvPeak;
|
||||
|
||||
if (Number.isNaN(suvPeak)) {
|
||||
uiNotificationService.show({
|
||||
title: 'SUV Peak',
|
||||
message: 'Segmented volume does not allow SUV Peak calculation',
|
||||
type: 'warning',
|
||||
});
|
||||
}
|
||||
const dataModifiedCallback = eventDetail => {
|
||||
debouncedHandleROIThresholding(eventDetail);
|
||||
};
|
||||
|
||||
eventTarget.addEventListenerDebounced(Enums.Events.SEGMENTATION_DATA_MODIFIED, callback, 250);
|
||||
const dataModifiedSubscription = segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
|
||||
dataModifiedCallback
|
||||
);
|
||||
|
||||
return () => {
|
||||
eventTarget.removeEventListenerDebounced(Enums.Events.SEGMENTATION_DATA_MODIFIED, callback);
|
||||
dataModifiedSubscription.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
}, [commandsManager, segmentationService]);
|
||||
|
||||
if (!activeSegmentation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tmtvValue = activeSegmentation.cachedStats?.tmtv?.value || null;
|
||||
const config = activeSegmentation.cachedStats?.tmtv?.config || {};
|
||||
|
||||
const actions = [
|
||||
{
|
||||
label: 'Export CSV',
|
||||
onClick: () => {
|
||||
commandsManager.runCommand('exportTMTVReportCSV', {
|
||||
segmentations,
|
||||
tmtv: tmtvValue,
|
||||
config,
|
||||
});
|
||||
},
|
||||
disabled: tmtvValue === null,
|
||||
},
|
||||
{
|
||||
label: 'Export RT Report',
|
||||
onClick: () => {
|
||||
commandsManager.runCommand('createTMTVRTReport');
|
||||
},
|
||||
disabled: tmtvValue === null,
|
||||
},
|
||||
];
|
||||
// Find the first segmentation with a TMTV value since all of them have the same value
|
||||
const tmtvSegmentation = segmentationsInfo.find(
|
||||
info => info.segmentation.cachedStats?.tmtv !== undefined
|
||||
);
|
||||
const tmtvValue = tmtvSegmentation?.segmentation.cachedStats?.tmtv;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-2 mb-10 flex flex-col">
|
||||
<div className="invisible-scrollbar overflow-y-auto overflow-x-hidden">
|
||||
{tmtvValue !== null ? (
|
||||
<div className="bg-secondary-dark 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-2 flex justify-center">
|
||||
<ActionButtons
|
||||
actions={actions}
|
||||
t={t}
|
||||
/>
|
||||
<div className="mt-2 mb-10 flex flex-col">
|
||||
<div className="invisible-scrollbar overflow-y-auto overflow-x-hidden">
|
||||
{tmtvValue !== null && tmtvValue !== undefined ? (
|
||||
<div className="bg-secondary-dark 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.toFixed(3)} mL`}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
className="absolute bottom-1 left-[50px] 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>
|
||||
</>
|
||||
</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,
|
||||
};
|
||||
+4
-8
@@ -9,14 +9,10 @@ function segmentationItemEditHandler({ id, servicesManager }: withAppTypes) {
|
||||
const onSubmitHandler = ({ action, value }) => {
|
||||
switch (action.id) {
|
||||
case 'save': {
|
||||
segmentationService.addOrUpdateSegmentation(
|
||||
{
|
||||
...segmentation,
|
||||
...value,
|
||||
},
|
||||
false, // don't suppress event
|
||||
true // it should update cornerstone
|
||||
);
|
||||
segmentationService.addOrUpdateSegmentation({
|
||||
...segmentation,
|
||||
...value,
|
||||
});
|
||||
}
|
||||
}
|
||||
uiDialogService.dismiss({ id: 'enter-annotation' });
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
PanelSegmentation,
|
||||
useActiveViewportSegmentationRepresentations,
|
||||
} from '@ohif/extension-cornerstone';
|
||||
import { Button, Icons } from '@ohif/ui-next';
|
||||
|
||||
export default function PanelTMTV({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
configuration,
|
||||
}: withAppTypes) {
|
||||
return (
|
||||
<>
|
||||
<PanelSegmentation
|
||||
servicesManager={servicesManager}
|
||||
commandsManager={commandsManager}
|
||||
extensionManager={extensionManager}
|
||||
configuration={configuration}
|
||||
>
|
||||
<ExportCSV
|
||||
servicesManager={servicesManager}
|
||||
commandsManager={commandsManager}
|
||||
/>
|
||||
</PanelSegmentation>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const ExportCSV = ({ servicesManager, commandsManager }: withAppTypes) => {
|
||||
const { segmentationsWithRepresentations: representations } =
|
||||
useActiveViewportSegmentationRepresentations({ servicesManager });
|
||||
|
||||
const tmtv = representations[0]?.segmentation.cachedStats?.tmtv;
|
||||
|
||||
const segmentations = representations.map(representation => representation.segmentation);
|
||||
|
||||
if (!segmentations.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-8 w-full items-center rounded pr-0.5">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="pl-1.5"
|
||||
onClick={() => {
|
||||
commandsManager.runCommand('exportTMTVReportCSV', {
|
||||
segmentations,
|
||||
tmtv,
|
||||
config: {},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icons.Download />
|
||||
<span className="pl-1">CSV</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -75,7 +75,7 @@ function RectangleROIOptions({ servicesManager, commandsManager }: withAppTypes)
|
||||
}, [selectedSegmentationId, config]);
|
||||
|
||||
useEffect(() => {
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
const segmentations = segmentationService.getSegmentationRepresentations();
|
||||
|
||||
if (!segmentations.length) {
|
||||
return;
|
||||
@@ -90,13 +90,12 @@ function RectangleROIOptions({ servicesManager, commandsManager }: withAppTypes)
|
||||
*/
|
||||
useEffect(() => {
|
||||
// ~~ Subscription
|
||||
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
|
||||
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
|
||||
const updated = segmentationService.EVENTS.SEGMENTATION_MODIFIED;
|
||||
const subscriptions = [];
|
||||
|
||||
[added, updated].forEach(evt => {
|
||||
[updated].forEach(evt => {
|
||||
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
const segmentations = segmentationService.getSegmentationRepresentations();
|
||||
|
||||
if (!segmentations.length) {
|
||||
return;
|
||||
|
||||
Reference in new issue
Block a user