OHIF with Cornerstone3D 2.0 (#4347)

This commit is contained in:
Alireza authored and GitHub committed 2024-11-06 07:15:27 -05:00
1 parent 8981a61b8c
commit 7ce7127d7c
803 files changed
+32760 -15893

No files matched your search

@@ -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,
};
@@ -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' });
+62
View File
@@ -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;