feat(hp priors): Study comparison hanging protocol (#3579)
This commit is contained in:
parent
ecbe7c56e0
commit
53c16b928f
@ -5,7 +5,6 @@ const segProtocol: Types.HangingProtocol.Protocol = {
|
||||
// Don't store this hanging protocol as it applies to the currently active
|
||||
// display set by default
|
||||
// cacheId: null,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'Segmentations',
|
||||
// Just apply this one when specifically listed
|
||||
protocolMatchingRules: [],
|
||||
|
||||
@ -5,7 +5,6 @@ const srProtocol: Types.HangingProtocol.Protocol = {
|
||||
// Don't store this hanging protocol as it applies to the currently active
|
||||
// display set by default
|
||||
// cacheId: null,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'SR Key Images',
|
||||
// Just apply this one when specifically listed
|
||||
protocolMatchingRules: [],
|
||||
|
||||
@ -4,9 +4,8 @@ const mpr: Types.HangingProtocol.Protocol = {
|
||||
id: 'mpr',
|
||||
name: 'Multi-Planar Reconstruction',
|
||||
locked: true,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
createdDate: '2021-02-23',
|
||||
modifiedDate: '2023-04-03',
|
||||
modifiedDate: '2023-08-15',
|
||||
availableTo: {},
|
||||
editableBy: {},
|
||||
// Unknown number of priors referenced - so just match any study
|
||||
@ -164,7 +163,6 @@ const mpr: Types.HangingProtocol.Protocol = {
|
||||
const mprAnd3DVolumeViewport = {
|
||||
id: 'mprAnd3DVolumeViewport',
|
||||
locked: true,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'mpr',
|
||||
createdDate: '2023-03-15T10:29:44.894Z',
|
||||
modifiedDate: '2023-03-15T10:29:44.894Z',
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import hpMNGrid from './hpMNGrid';
|
||||
import hpMNCompare from './hpCompare';
|
||||
|
||||
const defaultProtocol = {
|
||||
id: 'default',
|
||||
@ -6,7 +7,6 @@ const defaultProtocol = {
|
||||
// Don't store this hanging protocol as it applies to the currently active
|
||||
// display set by default
|
||||
// cacheId: null,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'Default',
|
||||
createdDate: '2021-02-23T19:22:08.894Z',
|
||||
modifiedDate: '2023-04-01',
|
||||
@ -108,6 +108,11 @@ function getHangingProtocolModule() {
|
||||
name: hpMNGrid.id,
|
||||
protocol: hpMNGrid,
|
||||
},
|
||||
// Create a MxN comparison hanging protocol available by default
|
||||
{
|
||||
name: hpMNCompare.id,
|
||||
protocol: hpMNCompare,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
193
extensions/default/src/hpCompare.ts
Normal file
193
extensions/default/src/hpCompare.ts
Normal file
@ -0,0 +1,193 @@
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
const defaultDisplaySetSelector = {
|
||||
studyMatchingRules: [
|
||||
{
|
||||
// The priorInstance is a study counter that indicates what position this study is in
|
||||
// and the value comes from the options parameter.
|
||||
attribute: 'studyInstanceUIDsIndex',
|
||||
from: 'options',
|
||||
required: true,
|
||||
constraint: {
|
||||
equals: { value: 0 },
|
||||
},
|
||||
},
|
||||
],
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
attribute: 'numImageFrames',
|
||||
constraint: {
|
||||
greaterThan: { value: 0 },
|
||||
},
|
||||
},
|
||||
// This display set will select the specified items by preference
|
||||
// It has no affect if nothing is specified in the URL.
|
||||
{
|
||||
attribute: 'isDisplaySetFromUrl',
|
||||
weight: 10,
|
||||
constraint: {
|
||||
equals: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const priorDisplaySetSelector = {
|
||||
studyMatchingRules: [
|
||||
{
|
||||
// The priorInstance is a study counter that indicates what position this study is in
|
||||
// and the value comes from the options parameter.
|
||||
attribute: 'studyInstanceUIDsIndex',
|
||||
from: 'options',
|
||||
required: true,
|
||||
constraint: {
|
||||
equals: { value: 1 },
|
||||
},
|
||||
},
|
||||
],
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
attribute: 'numImageFrames',
|
||||
constraint: {
|
||||
greaterThan: { value: 0 },
|
||||
},
|
||||
},
|
||||
// This display set will select the specified items by preference
|
||||
// It has no affect if nothing is specified in the URL.
|
||||
{
|
||||
attribute: 'isDisplaySetFromUrl',
|
||||
weight: 10,
|
||||
constraint: {
|
||||
equals: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const currentDisplaySet = {
|
||||
id: 'defaultDisplaySetId',
|
||||
};
|
||||
|
||||
const priorDisplaySet = {
|
||||
id: 'priorDisplaySetId',
|
||||
};
|
||||
|
||||
const currentViewport0 = {
|
||||
viewportOptions: {
|
||||
toolGroupId: 'default',
|
||||
allowUnmatchedView: true,
|
||||
},
|
||||
displaySets: [currentDisplaySet],
|
||||
};
|
||||
|
||||
const currentViewport1 = {
|
||||
...currentViewport0,
|
||||
displaySets: [
|
||||
{
|
||||
...currentDisplaySet,
|
||||
matchedDisplaySetsIndex: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const priorViewport0 = {
|
||||
...currentViewport0,
|
||||
displaySets: [priorDisplaySet],
|
||||
};
|
||||
|
||||
const priorViewport1 = {
|
||||
...priorViewport0,
|
||||
displaySets: [
|
||||
{
|
||||
...priorDisplaySet,
|
||||
matchedDisplaySetsIndex: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* This hanging protocol can be activated on the primary mode by directly
|
||||
* referencing it in a URL or by directly including it within a mode, e.g.:
|
||||
* `&hangingProtocolId=@ohif/mnGrid` added to the viewer URL
|
||||
* It is not included in the viewer mode by default.
|
||||
*/
|
||||
const hpMNCompare: Types.HangingProtocol.Protocol = {
|
||||
id: '@ohif/hpCompare',
|
||||
description: 'Compare two studies in various layouts',
|
||||
name: 'Compare Two Studies',
|
||||
numberOfPriorsReferenced: 1,
|
||||
protocolMatchingRules: [
|
||||
{
|
||||
id: 'Two Studies',
|
||||
weight: 1000,
|
||||
attribute: 'StudyInstanceUID',
|
||||
// The 'from' attribute says where to get the 'attribute' value from. In this case
|
||||
// prior means the second study in the study list.
|
||||
from: 'prior',
|
||||
required: true,
|
||||
constraint: {
|
||||
notNull: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
toolGroupIds: ['default'],
|
||||
displaySetSelectors: {
|
||||
defaultDisplaySetId: defaultDisplaySetSelector,
|
||||
priorDisplaySetId: priorDisplaySetSelector,
|
||||
},
|
||||
defaultViewport: {
|
||||
viewportOptions: {
|
||||
viewportType: 'stack',
|
||||
toolGroupId: 'default',
|
||||
allowUnmatchedView: true,
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'defaultDisplaySetId',
|
||||
matchedDisplaySetsIndex: -1,
|
||||
},
|
||||
],
|
||||
},
|
||||
stages: [
|
||||
{
|
||||
name: '2x2',
|
||||
stageActivation: {
|
||||
enabled: {
|
||||
minViewportsMatched: 4,
|
||||
},
|
||||
},
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: 2,
|
||||
columns: 2,
|
||||
},
|
||||
},
|
||||
viewports: [
|
||||
currentViewport0,
|
||||
priorViewport0,
|
||||
currentViewport1,
|
||||
priorViewport1,
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: '2x1',
|
||||
stageActivation: {
|
||||
enabled: {
|
||||
minViewportsMatched: 2,
|
||||
},
|
||||
},
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: 1,
|
||||
columns: 2,
|
||||
},
|
||||
},
|
||||
viewports: [currentViewport0, priorViewport0],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default hpMNCompare;
|
||||
@ -7,7 +7,6 @@ import { Types } from '@ohif/core';
|
||||
* It is not included in the viewer mode by default.
|
||||
*/
|
||||
const hpMN: Types.HangingProtocol.Protocol = {
|
||||
hasUpdatedPriorsInformation: false,
|
||||
id: '@ohif/mnGrid',
|
||||
description: 'Has various hanging protocol grid layouts',
|
||||
name: '2x2',
|
||||
|
||||
@ -217,7 +217,6 @@ const stage4 = {
|
||||
const ptCT = {
|
||||
id: '@ohif/extension-tmtv.hangingProtocolModule.ptCT',
|
||||
locked: true,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'Default',
|
||||
createdDate: '2021-02-23T19:22:08.894Z',
|
||||
modifiedDate: '2022-10-04T19:22:08.894Z',
|
||||
|
||||
28
platform/app/cypress/integration/MultiStudy.spec.js
Normal file
28
platform/app/cypress/integration/MultiStudy.spec.js
Normal file
@ -0,0 +1,28 @@
|
||||
describe('OHIF Multi Study', () => {
|
||||
const beforeSetup = () => {
|
||||
cy.checkStudyRouteInViewer(
|
||||
'1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1,1.2.840.113619.2.5.1762583153.215519.978957063.78',
|
||||
'&hangingProtocolId=@ohif/hpCompare'
|
||||
);
|
||||
cy.expectMinimumThumbnails(4);
|
||||
cy.initCornerstoneToolsAliases();
|
||||
cy.initCommonElementsAliases();
|
||||
};
|
||||
|
||||
it('Should display 2 comparison up', () => {
|
||||
beforeSetup();
|
||||
|
||||
cy.get('[data-cy="viewport-pane"]')
|
||||
.its('length')
|
||||
.should('be.eq', 4);
|
||||
cy.get('[data-cy="studyDate"]').should(studyDate => {
|
||||
expect(studyDate.length).to.be.eq(4);
|
||||
expect(studyDate.text())
|
||||
.to.contain('2014')
|
||||
.contain('2001');
|
||||
expect(studyDate.text().indexOf('2014')).to.be.lessThan(
|
||||
studyDate.text().indexOf('2001')
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -2,7 +2,6 @@ import HangingProtocolService from './HangingProtocolService';
|
||||
|
||||
const testProtocol = {
|
||||
id: 'test',
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'Default',
|
||||
protocolMatchingRules: [
|
||||
{
|
||||
|
||||
@ -161,6 +161,9 @@ export default class HangingProtocolService extends PubSubService {
|
||||
this.studies = [];
|
||||
this.viewportMatchDetails = new Map();
|
||||
this.displaySetMatchDetails = new Map();
|
||||
this.protocol = undefined;
|
||||
this.stageIndex = undefined;
|
||||
this.protocolEngine = undefined;
|
||||
}
|
||||
|
||||
/** Leave the hanging protocol in the initialized state */
|
||||
@ -908,7 +911,7 @@ export default class HangingProtocolService extends PubSubService {
|
||||
const stage = stages[i];
|
||||
if (stage.id === stageId && stage.status !== 'disabled') {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -1221,7 +1224,7 @@ export default class HangingProtocolService extends PubSubService {
|
||||
for (const subMatch of match.matchingScores) {
|
||||
if (subMatch.displaySetInstanceUID === displaySetUID) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`Reused viewport details ${id} with ds ${displaySetUID} not valid`
|
||||
@ -1421,25 +1424,26 @@ export default class HangingProtocolService extends PubSubService {
|
||||
seriesMatchingRules
|
||||
);
|
||||
const matchActiveOnly = this.protocol.numberOfPriorsReferenced === -1;
|
||||
this.studies.forEach(study => {
|
||||
this.studies.forEach((study, studyInstanceUIDsIndex) => {
|
||||
// Skip non-active if active only
|
||||
if (matchActiveOnly && this.activeStudy !== study) {
|
||||
return;
|
||||
}
|
||||
|
||||
const studyDisplaySets = this.displaySets.filter(
|
||||
it => it.StudyInstanceUID === study.StudyInstanceUID
|
||||
it => it.StudyInstanceUID === study.StudyInstanceUID
|
||||
);
|
||||
|
||||
const studyMatchDetails = this.protocolEngine.findMatch(
|
||||
study,
|
||||
studyMatchingRules,
|
||||
{
|
||||
studies: this.studies,
|
||||
displaySets: studyDisplaySets,
|
||||
allDisplaySets: this.displaySets,
|
||||
displaySetMatchDetails: this.displaySetMatchDetails,
|
||||
}
|
||||
study,
|
||||
studyMatchingRules,
|
||||
{
|
||||
studies: this.studies,
|
||||
displaySets: studyDisplaySets,
|
||||
allDisplaySets: this.displaySets,
|
||||
displaySetMatchDetails: this.displaySetMatchDetails,
|
||||
studyInstanceUIDsIndex,
|
||||
}
|
||||
);
|
||||
|
||||
// Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
||||
@ -1448,10 +1452,10 @@ export default class HangingProtocolService extends PubSubService {
|
||||
}
|
||||
|
||||
this.debug(
|
||||
'study',
|
||||
study.StudyInstanceUID,
|
||||
'display sets #',
|
||||
studyDisplaySets.length
|
||||
'study',
|
||||
study.StudyInstanceUID,
|
||||
'display sets #',
|
||||
studyDisplaySets.length
|
||||
);
|
||||
studyDisplaySets.forEach(displaySet => {
|
||||
const {
|
||||
@ -1460,15 +1464,15 @@ export default class HangingProtocolService extends PubSubService {
|
||||
displaySetInstanceUID,
|
||||
} = displaySet;
|
||||
const seriesMatchDetails = this.protocolEngine.findMatch(
|
||||
displaySet,
|
||||
seriesMatchingRules,
|
||||
// Todo: why we have images here since the matching type does not have it
|
||||
{
|
||||
studies: this.studies,
|
||||
instance: displaySet.images?.[0],
|
||||
displaySetMatchDetails: this.displaySetMatchDetails,
|
||||
displaySets: studyDisplaySets,
|
||||
}
|
||||
displaySet,
|
||||
seriesMatchingRules,
|
||||
// Todo: why we have images here since the matching type does not have it
|
||||
{
|
||||
studies: this.studies,
|
||||
instance: displaySet.images?.[0],
|
||||
displaySetMatchDetails: this.displaySetMatchDetails,
|
||||
displaySets: studyDisplaySets,
|
||||
}
|
||||
);
|
||||
|
||||
// Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
||||
|
||||
@ -270,7 +270,6 @@ export type Protocol = {
|
||||
stages: ProtocolStage[];
|
||||
// Optional
|
||||
locked?: boolean;
|
||||
hasUpdatedPriorsInformation?: boolean;
|
||||
name?: string;
|
||||
createdDate?: string;
|
||||
modifiedDate?: string;
|
||||
@ -284,7 +283,9 @@ export type Protocol = {
|
||||
/* The number of priors required for this hanging protocol.
|
||||
* -1 means that NO priors are referenced, and thus this HP matches
|
||||
* only the active study, whereas 0 means that an unknown number of
|
||||
* priors is matched.
|
||||
* priors is matched. Positive values mean at least that many priors are
|
||||
* required.
|
||||
* Replaces hasUpdatedPriors
|
||||
*/
|
||||
numberOfPriorsReferenced?: number;
|
||||
syncDataForViewports?: boolean;
|
||||
|
||||
@ -36,7 +36,6 @@ Here is an example protocol which if used will hang a 1x3 layout with the first
|
||||
const oneByThreeProtocol = {
|
||||
id: 'oneByThreeProtocol',
|
||||
locked: true,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'Default',
|
||||
createdDate: '2021-02-23T19:22:08.894Z',
|
||||
modifiedDate: '2022-10-04T19:22:08.894Z',
|
||||
@ -241,6 +240,9 @@ A list of criteria for the protocol along with the provided points for ranking.
|
||||
"StudyDescription", "ModalitiesInStudy", "NumberOfStudyRelatedSeries", "NumberOfSeriesRelatedInstances"
|
||||
In addition to these tags, you can also use a custom attribute that you have registered before.
|
||||
We will learn more about this later.
|
||||
- `from`: Indicates the source of the attribute. This allows getting values
|
||||
from other objects such as the `prior` instance object instead of from the
|
||||
current one.
|
||||
|
||||
|
||||
|
||||
@ -279,6 +281,20 @@ A list of criteria for the protocol along with the provided points for ranking.
|
||||
},
|
||||
```
|
||||
|
||||
### `from` attribute
|
||||
The from attribute allows getting the attribute to test from some other object
|
||||
such as the prior study, the list of studies overall or another module provided
|
||||
value. Some of the possible attributes are:
|
||||
|
||||
* `prior`: To get the value from the prior study.
|
||||
* `activeStudy`: To match the active study
|
||||
* `studies`: To match the list of studies to display
|
||||
* `displaySets`: The display sets for the current study
|
||||
* `allDisplaySets`: Alll available display sets
|
||||
* `instance`: An instance from the current display set being tested
|
||||
* `options`: Gets the options object itself, eg if you want a simple top level
|
||||
value.
|
||||
|
||||
### displaySetSelectors
|
||||
Defines the display sets that the protocol will use for arrangement.
|
||||
|
||||
@ -475,22 +491,70 @@ HangingProtocolService.addCustomAttribute(
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Matching on Prior Study with UID
|
||||
|
||||
Often it is desired to match a new study to a prior study (e.g., follow up on
|
||||
a surgery). Since the hanging protocols run on displaySets we need to have a
|
||||
way to let OHIF knows that it needs to load the prior study as well. This can
|
||||
be done by specifying both StudyInstanceUIDs in the URL. Below we are
|
||||
running OHIF with two studies
|
||||
be done by specifying both StudyInstanceUIDs in the URL. The additional studies
|
||||
are then accessible to the hanging protocol. Below we are
|
||||
running OHIF with two studies, and a comparison hanging protocol available by
|
||||
default.
|
||||
|
||||
```bash
|
||||
http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095722.1
|
||||
http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095722.1&hangingprotocolId=@ohif/hpCompare
|
||||
```
|
||||
|
||||
Now you have access to both studies and you can use matchingRules to match
|
||||
displaySets.
|
||||
The `&hangingProtocolId` option forces the specific hanging protocol to be
|
||||
applied, but the mode can also add the hanging protocols to the default set,
|
||||
and then the best matching hanging protocol will be applied by the run method.
|
||||
|
||||
To match any other studies, it is required to enable the prior matching rules
|
||||
capability using:
|
||||
|
||||
```javascript
|
||||
// Indicate number of priors used - 0 means any number, -1 means none.
|
||||
numberOfPriorsReferenced: 1,
|
||||
```
|
||||
|
||||
Our roadmap includes enabling matching on prior studies without the UID (e.g., baseline, most recent and index).
|
||||
The matching rule that allows the hanging protocol to be runnable is:
|
||||
|
||||
```javascript
|
||||
protocolMatchingRules: [
|
||||
{
|
||||
id: 'Two Studies',
|
||||
weight: 1000,
|
||||
// This will generate 1.3.6.1.4.1.25403.345050719074.3824.20170125095722.1
|
||||
// since that is study instance UID in the prior from instance.
|
||||
attribute: 'StudyInstanceUID',
|
||||
// The 'from' attribute says where to get the 'attribute' value from. In this case
|
||||
// prior means the second study in the study list.
|
||||
from: 'prior',
|
||||
required: true,
|
||||
constraint: {
|
||||
notNull: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
The display set selector selecting the specific study to display is included
|
||||
in the studyMatchingRules. Note that this rule will cause ONLY the second study
|
||||
to be matched, so it won't attempt to match anything in other studies.
|
||||
Additional series level criteria, such as modality rules must be included at the
|
||||
`seriesMatchingRules`.
|
||||
|
||||
```javascript
|
||||
studyMatchingRules: [
|
||||
{
|
||||
// The priorInstance is a study counter that indicates what position this study is in
|
||||
// and the value comes from the options parameter.
|
||||
attribute: 'studyInstanceUIDsIndex',
|
||||
from: 'options',
|
||||
required: true,
|
||||
constraint: {
|
||||
equals: { value: 1 },
|
||||
},
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
@ -133,7 +133,11 @@ const ViewportActionBar = ({
|
||||
<span className="ml-1 text-aqua-pale text-large">{label}</span>
|
||||
)}
|
||||
<div className={separatorClasses}></div>
|
||||
<span ref={studyDateElemRef} className={studyDateClasses()}>
|
||||
<span
|
||||
data-cy="studyDate"
|
||||
ref={studyDateElemRef}
|
||||
className={studyDateClasses()}
|
||||
>
|
||||
{studyDate}
|
||||
</span>
|
||||
{showSeriesDesc && (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user