feat: Add interleaved HTJ2K and volume progressive loading (#4276)

This commit is contained in:
Bill Wallace 2024-07-05 12:33:07 -04:00 committed by GitHub
parent 2845f07806
commit a2084f319b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 336 additions and 17 deletions

View File

@ -42,6 +42,7 @@ import { createFrameViewSynchronizer } from './synchronizers/frameViewSynchroniz
const { helpers: volumeLoaderHelpers } = csStreamingImageVolumeLoader;
const { getDynamicVolumeInfo } = volumeLoaderHelpers ?? {};
const { imageRetrieveMetadataProvider } = cornerstone.utilities;
const Component = React.lazy(() => {
return import(/* webpackPrefetch: true */ './Viewport/OHIFCornerstoneViewport');
@ -55,6 +56,15 @@ const OHIFCornerstoneViewport = props => {
);
};
const stackRetrieveOptions = {
retrieveOptions: {
single: {
streaming: true,
decodeLevel: 1,
},
},
};
/**
*
*/
@ -80,6 +90,21 @@ const cornerstoneExtension: Types.Extensions.Extension = {
toolbarService.registerEventForToolbarUpdate(cornerstone.eventTarget, [
cornerstoneTools.Enums.Events.TOOL_ACTIVATED,
]);
// Configure the interleaved/HTJ2K loader
imageRetrieveMetadataProvider.clear();
// The default volume interleaved options are to interleave the
// image retrieve, but don't perform progressive loading per image
// This interleaves images and replicates them for low-resolution depth volume
// reconstruction, which progressively improves
imageRetrieveMetadataProvider.add(
'volume',
cornerstone.ProgressiveRetrieveImages.interleavedRetrieveStages
);
// The default stack loading option is to progressive load HTJ2K images
// There are other possible options, but these need more thought about
// how to define them.
imageRetrieveMetadataProvider.add('stack', stackRetrieveOptions);
},
onModeExit: ({ servicesManager }: withAppTypes): void => {

View File

@ -99,7 +99,7 @@ export default async function init({
hangingProtocolService,
viewportGridService,
stateSyncService,
studyPrefetcherService
studyPrefetcherService,
} = servicesManager.services;
window.services = servicesManager.services;
@ -180,10 +180,12 @@ export default async function init({
); // this provider is required for Calibration tool
metaData.addProvider(metadataProvider.get.bind(metadataProvider), 9999);
// These are set reasonably low to allow for interleaved retrieves and slower
// connections.
imageLoadPoolManager.maxNumRequests = {
interaction: appConfig?.maxNumRequests?.interaction || 100,
thumbnail: appConfig?.maxNumRequests?.thumbnail || 75,
prefetch: appConfig?.maxNumRequests?.prefetch || 10,
interaction: appConfig?.maxNumRequests?.interaction || 10,
thumbnail: appConfig?.maxNumRequests?.thumbnail || 5,
prefetch: appConfig?.maxNumRequests?.prefetch || 5,
};
initWADOImageLoader(userAuthenticationService, appConfig, extensionManager);

View File

@ -31,6 +31,7 @@ function PanelStudyBrowser({
const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([
...StudyInstanceUIDs,
]);
const [hasLoadedViewports, setHasLoadedViewports] = useState(false);
const [studyDisplayList, setStudyDisplayList] = useState([]);
const [displaySets, setDisplaySets] = useState([]);
const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({});
@ -108,6 +109,18 @@ function PanelStudyBrowser({
// // ~~ Initial Thumbnails
useEffect(() => {
if (!hasLoadedViewports) {
if (activeViewportId) {
// Once there is an active viewport id, it means the layout is ready
// so wait a bit of time to allow the viewports preferential loading
// which improves user experience of responsiveness significantly on slower
// systems.
window.setTimeout(() => setHasLoadedViewports(true), 250);
}
return;
}
const currentDisplaySets = displaySetService.activeDisplaySets;
currentDisplaySets.forEach(async dSet => {
const newImageSrcEntry = {};
@ -126,7 +139,14 @@ function PanelStudyBrowser({
return { ...prevState, ...newImageSrcEntry };
});
});
}, [StudyInstanceUIDs, dataSource, displaySetService, getImageSrc]);
}, [
StudyInstanceUIDs,
dataSource,
displaySetService,
getImageSrc,
hasLoadedViewports,
activeViewportId,
]);
// ~~ displaySets
useEffect(() => {
@ -144,6 +164,9 @@ function PanelStudyBrowser({
const SubscriptionDisplaySetsAdded = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
data => {
if (!hasLoadedViewports) {
return;
}
const { displaySetsAdded, options } = data;
displaySetsAdded.forEach(async dSet => {
const newImageSrcEntry = {};
@ -213,7 +236,7 @@ function PanelStudyBrowser({
const shouldCollapseStudy = expandedStudyInstanceUIDs.includes(StudyInstanceUID);
const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy
? // eslint-disable-next-line prettier/prettier
[...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)]
[...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)]
: [...expandedStudyInstanceUIDs, StudyInstanceUID];
setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs);

View File

@ -31,7 +31,7 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
}),
},
{
name: 'measure',
name: 'measurements',
iconName: 'tab-linear',
iconLabel: 'Measure',
label: i18n.t('SidePanel:Measurements'),

View File

@ -43,6 +43,7 @@ function PanelStudyBrowserTracking({
...StudyInstanceUIDs,
]);
const [studyDisplayList, setStudyDisplayList] = useState([]);
const [hasLoadedViewports, setHasLoadedViewports] = useState(false);
const [displaySets, setDisplaySets] = useState([]);
const [displaySetsLoadingState, setDisplaySetsLoadingState] = useState({});
const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({});
@ -128,6 +129,18 @@ function PanelStudyBrowserTracking({
// ~~ Initial Thumbnails
useEffect(() => {
if (!hasLoadedViewports) {
if (activeViewportId) {
// Once there is an active viewport id, it means the layout is ready
// so wait a bit of time to allow the viewports preferential loading
// which improves user experience of responsiveness significantly on slower
// systems.
window.setTimeout(() => setHasLoadedViewports(true), 250);
}
return;
}
const currentDisplaySets = displaySetService.activeDisplaySets;
if (!currentDisplaySets.length) {
@ -152,7 +165,7 @@ function PanelStudyBrowserTracking({
return { ...prevState, ...newImageSrcEntry };
});
});
}, [displaySetService, dataSource, getImageSrc]);
}, [displaySetService, dataSource, getImageSrc, activeViewportId, hasLoadedViewports]);
// ~~ displaySets
useEffect(() => {
@ -209,6 +222,9 @@ function PanelStudyBrowserTracking({
const SubscriptionDisplaySetsAdded = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
data => {
if (!hasLoadedViewports) {
return;
}
const { displaySetsAdded, options } = data;
displaySetsAdded.forEach(async dSet => {
const displaySetInstanceUID = dSet.displaySetInstanceUID;
@ -428,7 +444,7 @@ function PanelStudyBrowserTracking({
onClickUntrack={displaySetInstanceUID => {
onClickUntrack(displaySetInstanceUID);
}}
onClickThumbnail={() => {}}
onClickThumbnail={() => { }}
onDoubleClickThumbnail={onDoubleClickThumbnailHandler}
activeDisplaySetInstanceUIDs={activeViewportDisplaySetInstanceUIDs}
/>

View File

@ -13,6 +13,7 @@ const ohif = {
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
thumbnailList: '@ohif/extension-default.panelModule.seriesList',
measurements: '@ohif/extension-default.panelModule.measurements',
};
const tracked = {
@ -134,6 +135,9 @@ function modeFactory() {
return {
id: ohif.layout,
props: {
// Use the first two for an untracked view
// leftPanels: [ohif.thumbnailList],
// rightPanels: [dicomSeg.panel, ohif.measurements],
leftPanels: [tracked.thumbnailList],
rightPanels: [dicomSeg.panel, tracked.measurements],
// rightPanelClosed: true, // optional prop to start with collapse panels

View File

@ -2,7 +2,8 @@ import MODULE_TYPES from './MODULE_TYPES';
import log from '../log';
import { PubSubService, ServiceProvidersManager } from '../services';
import { HotkeysManager, CommandsManager } from '../classes';
import { DataSourceDefinition } from '../types';
import type { DataSourceDefinition } from '../types';
import type AppTypes from '../types/AppTypes';
/**
* This is the arguments given to create the extension.
@ -48,7 +49,7 @@ export interface Extension {
getSopClassHandlerModule?: (p: ExtensionParams) => unknown;
getToolbarModule?: (p: ExtensionParams) => unknown;
getPanelModule?: (p: ExtensionParams) => unknown;
onModeEnter?: () => void;
onModeEnter?: (p: AppTypes) => void;
onModeExit?: () => void;
}

View File

@ -1,5 +1,5 @@
---
sidebar_position: 1
sidebar_position: 2
sidebar_label: 3.7 -> 3.8
---

View File

@ -0,0 +1,215 @@
---
sidebar_position: 1
sidebar_label: 3.8 -> 3.9-beta
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Migration Guide
## React 18 Migration Guide
As we upgrade to React 18, we're making some exciting changes to improve performance and developer experience. This guide will help you navigate the key updates and ensure your custom extensions and modes are compatible with the new version.
What's Changing?
<Tabs>
<TabItem value="Before" label="Before" default>
```md
- React 17
- Using `defaultProps`
- `babel-inline-svg` for SVG imports
```
</TabItem>
<TabItem value="After" label="After">
```md
- React 18
- Default parameters for props
- `svgr` for SVG imports
```
</TabItem>
</Tabs>
### Update React version:
In your custom extensions and modes, change the version of react and react-dom to ^18.3.1.
### Replace defaultProps with default parameters:
<Tabs>
<TabItem value="Before" label="Before" default>
```jsx
const MyComponent = ({ prop1, prop2 }) => {
return <div>{prop1} {prop2}</div>
}
MyComponent.defaultProps = {
prop1: 'default value',
prop2: 'default value'
}
```
</TabItem>
<TabItem value="After" label="After">
```jsx
const MyComponent = ({ prop1 = 'default value', prop2 = 'default value' }) => {
return <div>{prop1} {prop2}</div>
}
```
</TabItem>
</Tabs>
### Update SVG imports:
<Tabs>
<TabItem value="Before" label="Before" default>
```javascript
import arrowDown from './../../assets/icons/arrow-down.svg';
```
</TabItem>
<TabItem value="After" label="After">
```javascript
import { ReactComponent as arrowDown } from './../../assets/icons/arrow-down.svg';
```
</TabItem>
</Tabs>
<br/>
---
<br/>
## Renaming
The panel in the default extension is renamed from `measure` to `measurements` to be more consistent with the rest of the extensions.
**Action Needed**
Update any references to the `measure` panel to `measurements` in your code.
Find and replace
<Tabs>
<TabItem value="Before" label="Before 🕰️" default>
@ohif/extension-default.panelModule.measure
</TabItem>
<TabItem value="After" label="After 🚀" >
@ohif/extension-default.panelModule.measurements
</TabItem>
</Tabs>
<br/>
---
<br/>
## RTStructure Set has transitioned from VTK actors to SVG.
We have transitioned from VTK-based rendering to SVG-based rendering for RTStructure Set contours. This change should not require any modifications to your codebase. We anticipate improved stability and speed in our contour rendering.
As a result of this update, viewports rendering RTStructure Sets will no longer convert to volume viewports. Instead, they will remain as stack viewports.
Read more in Pull Requests:
- https://github.com/OHIF/Viewers/pull/4074
- https://github.com/OHIF/Viewers/pull/4157
<br/>
---
<br/>
## Crosshairs
They now have new colors in their associated viewports in the MPR view. However, you can turn this feature off.
To disable it, remove the configuration from the `initToolGroups` in your mode.
```
{
configuration: {
viewportIndicators: true,
viewportIndicatorsConfig: {
circleRadius: 5,
xOffset: 0.95,
yOffset: 0.05,
},
}
}
```
<br/>
---
<br/>
## BulkDataURI Configuration
We've updated the configuration for BulkDataURI to provide more flexibility and control. This guide will help you migrate from the old configuration to the new one.
### What's Changing?
<Tabs>
<TabItem value="Before" label="Before 🕰️" default>
```javascript
useBulkDataURI: false,
```
</TabItem>
<TabItem value="After" label="After 🚀">
```javascript
bulkDataURI: {
enabled: true,
// Additional configuration options
},
```
</TabItem>
</Tabs>
Additional Notes:
- The new configuration allows for more granular control over BulkDataURI behavior.
- You can now add custom URL prefixing logic using the startsWith and prefixWith properties.
- This change enables easier correction of retrieval URLs, especially in scenarios where URLs pass through multiple systems.
<br/>
---
<br/>
## Polyfill.io
We have removed the Polyfill.io script from the Viewer. If you require polyfills, you can add them to your project manually. This change primarily affects Internet Explorer, which Microsoft has already [ended support for](https://learn.microsoft.com/en-us/lifecycle/faq/internet-explorer-microsoft-edge#is-internet-explorer-11-the-last-version-of-internet-explorer-).
<br/>
---
<br/>
## Dynamic Modules
TBD

View File

@ -1,5 +1,5 @@
---
sidebar_position: 2
sidebar_position: 3
sidebar_label: 2.x -> 3.5
---

View File

@ -2066,7 +2066,7 @@
"@docusaurus/theme-search-algolia" "2.4.3"
"@docusaurus/types" "2.4.3"
"@docusaurus/react-loadable@5.5.2", "react-loadable@npm:@docusaurus/react-loadable@5.5.2":
"@docusaurus/react-loadable@5.5.2":
version "5.5.2"
resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce"
integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==
@ -17966,6 +17966,14 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1:
dependencies:
"@babel/runtime" "^7.10.3"
"react-loadable@npm:@docusaurus/react-loadable@5.5.2":
version "5.5.2"
resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce"
integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==
dependencies:
"@types/react" "*"
prop-types "^15.6.2"
react-modal@3.11.2:
version "3.11.2"
resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.11.2.tgz#bad911976d4add31aa30dba8a41d11e21c4ac8a4"
@ -19764,7 +19772,7 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -19782,6 +19790,15 @@ string-width@^1.0.1:
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
@ -19868,7 +19885,7 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -19889,6 +19906,13 @@ strip-ansi@^4.0.0:
dependencies:
ansi-regex "^3.0.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.0, strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@ -21961,7 +21985,7 @@ worker-loader@3.0.8, worker-loader@^3.0.8:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -21987,6 +22011,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"