Custom Hanging Protocol
Write a script to dynamically arrange the viewports for your project
The Custom Hanging Protocol feature allows you to write a script that will programmatically define the visual layout of your Annotation Tool at the Project level.
Pre-configuring parameters such as Windowing settings, Thresholding settings, the number of viewports in a Layout Tab, which views display by default, etc., is both an easy way to save time for your annotators and makes for a much smoother overall annotation experience.
This guide provides an overview of the available functions and types to help you effectively manage these settings. At present, you can control the following:
- The dimensions of a Layout Tab (
setDimensions
):- REQUIRED: the number of columns in a Layout Tab (
numColumns
) - REQUIRED: the number of rows in a Layout Tab (
numRows
)
- The contents of each viewport in a Layout Tab (
setViews
):- REQUIRED: an array describing each viewport's content (
views
) - REQUIRED: Which series to show (
seriesIndex
) - REQUIRED: Which way to view the series (
plane
) - Flip the view horizontally (
flippedHorizontally
) - Flip the view vertically (
flippedVertically
) - Activate Intellisync (
synchronized
) - Maximize a single viewport in a Layout Tab (
expanded
)
- The default Windowing setting for each Series (
setWindowing
):- REQUIRED: the number of the Series (
seriesIndex
) - REQUIRED: the desired Windowing Level (
level
) - REQUIRED: the desired Windowing Width (
width
)
- The default Thresholding setting for each Series (
setThresholding
):- REQUIRED: the number of the Series (
seriesIndex
) - REQUIRED: the lower limit of the Thresholding range (
min
) - REQUIRED: the upper limit of the Thresholding range (
max
)
- Create and configure a new Layout Tab in your Task (
nextTab
) - Configuration settings for the Annotation Tool (
setSegmentationSettings
)*
The Custom Hanging Protocol script takes the available Series for a particular Task as input and returns the layout dimensions and list of views to display.
function setViews(views: View[]) {
//...
}
function setDimensions(numColumns: number, numRows: number) {
// ...
}
function setWindowing(seriesIndex: number, level: number, width: number){
// ...
}
function setThresholding(seriesIndex: number, min: number, max: number) {
// ...
}
function nextTab()
// this function has been replaced by the Tool Settings page of Project Settings
function setSegmentationSettings(
[
{
toolName: ToolOptions;
enabled: boolean;
modes?: ToolModes[];
defaultMode?: ToolModes;
defaultTool?: boolean;
}
]
);
// When a user uploads a Task and enables Hanging Protocols,
// the hangingProtocol() function takes Series[] and the following parameters as input
interface Series {
seriesIndex: number;
is2DImage: boolean;
isVideo: boolean;
numFrames: number;
name: string; // User defined name if available, else "A", "B", ...
imagingAxis: 'AXIAL' | 'SAGITTAL' | 'CORONAL';
}
interface View {
plane: 'AXIAL' | 'SAGITTAL' | 'CORONAL' | '3D' | 'MIP';
seriesIndex: number;
flippedHorizontally?: boolean;
flippedVertically?: boolean;
synchronized?: boolean;
expanded?: boolean; // Only applicable to a single view in a given Layout Tab
}
This default script uses some defined macros to make setting the view easier.
function hangingProtocol(allSeries: Series[]) {
// This is the default layout script
if (allSeries.length > 1) {
setMultiSeries();
} else if (allSeries[0].is2DImage) {
setSingleView();
} else {
setMPR();
}
}
function setSingleView(seriesIndex=0) {
setDimensions(1,1);
setViews([
{
plane: allSeries[seriesIndex].imagingAxis,
seriesIndex: seriesIndex,
}
]);
}
This script sets each Series as a single viewport that is viewed on the imaging axis.
function setMultiSeries() {
function singleSeries(series_, index) {
return {
plane: series_.imagingAxis,
seriesIndex: index,
};
}
let views = allSeries.map(singleSeries);
setViews(views.slice(0,9));
}
function setMPR(seriesIndex=0) {
let targetSeries = allSeries[seriesIndex];
setDimensions(2,2);
setViews([
{
plane: 'SAGITTAL',
seriesIndex: seriesIndex,
expanded: targetSeries.imagingAxis === 'SAGITTAL',
},
{
plane: 'CORONAL',
seriesIndex: seriesIndex,
expanded: targetSeries.imagingAxis === 'CORONAL',
},
{
plane: 'AXIAL',
seriesIndex: seriesIndex,
expanded: targetSeries.imagingAxis === 'AXIAL',
},
{
plane: '3D',
seriesIndex: seriesIndex,
}
]);
}
The following script creates 2 Layout Tabs, each containing 2 Series.
if (allSeries.length === 4) { // executes when there are 4 total Series in a Task
setDimensions(2, 1); // set 2x1 layout for Layout Tab 1
setViews( // adding the first and second image/volume to Layout Tab 1
[
{
seriesIndex: 0,
plane: 'SAGITTAL'
},
{
seriesIndex: 1,
plane: 'SAGITTAL'
}
]
);
nextTab(); // create and configure Layout Tab 2
setDimensions(2, 1); // set 2x1 layout for Layout Tab 2
setViews( // add third and fourth image/volume to Layout Tab 2
[
{
seriesIndex: 2,
plane: 'SAGITTAL'
},
{
seriesIndex: 3,
plane: 'SAGITTAL'
}
]
)
Hanging protocols can be used along side Intellisyncfor ease of use when annotating scans in a study.
For example, let's assume that we have uploaded a single Task containing 4 Series from an MRI study: T1, T1CE, T2, and Flair weighted MR scans.
After we enable Hanging Protocols, the
hangingProtocol()
function will take the 4 Series as an input and parse them in the following way:[
{
seriesIndex: 0,
is2DImage: false,
isVideo: false,
numFrames: 1,
name: 'T1',
imagingAxis: 'SAGITTAL',
},
{
seriesIndex: 1,
is2DImage: false,
isVideo: false,
numFrames: 1,
name: 'T2',
imagingAxis: 'SAGITTAL',
},
{
seriesIndex: 2,
is2DImage: false,
isVideo: false,
numFrames: 1,
name: 'T1CE',
imagingAxis: 'SAGITTAL',
},
{
seriesIndex: 3,
is2DImage: false,
isVideo: false,
numFrames: 1,
name: 'Flair',
imagingAxis: 'SAGITTAL',
},
]
We can then use the information that has been parsed by the
hangingProtocol()
function to generate a script that sorts our views, displays the imaging axis and activates Intellisync.// sort series by Name
let priorities = ['t1', 't1ce', 't2', 'flair'];
allSeries.sort((a, b)=>priorities.indexOf(a.name.toLowerCase()) - priorities.indexOf(b.name.toLowerCase()));
// display Series along the imaging axis
let imagingAxis = allSeries[0].imagingAxis;
// filter out views that were imaged in a different axis
let eligibleSeries = allSeries.filter((series) => series.imagingAxis === imagingAxis);
// Configure viewports
setViews(eligibleSeries.map((series) => {
return {
seriesIndex: series.seriesIndex,
plane: series.imagingAxis,
synchronized: true,
};
}));
Last modified 1mo ago