feat: Ultrasound Pleura B-line Annotation Mode & Extension (#5061)

This commit is contained in:
rodrigobasilio2022 2025-06-24 13:50:48 -03:00 committed by GitHub
parent 9b8eef9474
commit 226f9b0773
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 7123 additions and 3660 deletions

833
bun.lock

File diff suppressed because it is too large Load Diff

View File

@ -46,8 +46,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.15.6",
"@cornerstonejs/core": "^3.15.6",
"@cornerstonejs/adapters": "^3.16.0",
"@cornerstonejs/core": "^3.16.0",
"@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3"
}

View File

@ -46,8 +46,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.15.6",
"@cornerstonejs/core": "^3.15.6",
"@cornerstonejs/adapters": "^3.16.0",
"@cornerstonejs/core": "^3.16.0",
"@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3"
}

View File

@ -46,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.15.6",
"@cornerstonejs/core": "^3.15.6",
"@cornerstonejs/tools": "^3.15.6",
"@cornerstonejs/adapters": "^3.16.0",
"@cornerstonejs/core": "^3.16.0",
"@cornerstonejs/tools": "^3.16.0",
"classnames": "^2.3.2"
}
}

View File

@ -42,8 +42,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^3.15.6",
"@cornerstonejs/tools": "^3.15.6",
"@cornerstonejs/core": "^3.16.0",
"@cornerstonejs/tools": "^3.16.0",
"classnames": "^2.3.2"
}
}

View File

@ -38,7 +38,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.15.6",
"@cornerstonejs/dicom-image-loader": "^3.16.0",
"@ohif/core": "3.11.0-beta.60",
"@ohif/ui": "3.11.0-beta.60",
"dcmjs": "*",
@ -51,12 +51,12 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.15.6",
"@cornerstonejs/ai": "^3.15.6",
"@cornerstonejs/core": "^3.15.6",
"@cornerstonejs/labelmap-interpolation": "^3.15.6",
"@cornerstonejs/polymorphic-segmentation": "^3.15.6",
"@cornerstonejs/tools": "^3.15.6",
"@cornerstonejs/adapters": "^3.16.0",
"@cornerstonejs/ai": "^3.16.0",
"@cornerstonejs/core": "^3.16.0",
"@cornerstonejs/labelmap-interpolation": "^3.16.0",
"@cornerstonejs/polymorphic-segmentation": "^3.16.0",
"@cornerstonejs/tools": "^3.16.0",
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
"@kitware/vtk.js": "32.12.0",
"html2canvas": "^1.4.1",

View File

@ -1,7 +1,8 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { vec3 } from 'gl-matrix';
import PropTypes from 'prop-types';
import { metaData, Enums, utilities } from '@cornerstonejs/core';
import { metaData, Enums, utilities, eventTarget } from '@cornerstonejs/core';
import { Enums as csToolsEnums, UltrasoundPleuraBLineTool } from '@cornerstonejs/tools';
import type { ImageSliceData } from '@cornerstonejs/core/types';
import { ViewportOverlay } from '@ohif/ui-next';
import type { InstanceMetadata } from '@ohif/core/src/types';
@ -68,6 +69,7 @@ function CustomizableViewportOverlay({
servicesManager.services;
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
const [scale, setScale] = useState(1);
const [annotationState, setAnnotationState] = useState(0);
const { isViewportBackgroundLight: isLight } = useViewportRendering(viewportId);
const { imageIndex } = imageSliceData;
@ -132,6 +134,20 @@ function CustomizableViewportOverlay({
};
}, [viewportId, viewportData, voi, element]);
const annotationModified = useCallback(evt => {
if (evt.detail.annotation.metadata.toolName === UltrasoundPleuraBLineTool.toolName) {
// Update the annotation state to trigger a re-render
setAnnotationState(prevState => prevState + 1);
}
}, []);
useEffect(() => {
eventTarget.addEventListener(csToolsEnums.Events.ANNOTATION_MODIFIED, annotationModified);
return () => {
eventTarget.removeEventListener(csToolsEnums.Events.ANNOTATION_MODIFIED, annotationModified);
};
}, [annotationModified]);
/**
* Updating the scale when the viewport changes its zoom
*/
@ -209,6 +225,7 @@ function CustomizableViewportOverlay({
voi,
scale,
instanceNumber,
annotationState,
]
);

View File

@ -31,6 +31,7 @@ import {
TrackballRotateTool,
AdvancedMagnifyTool,
UltrasoundDirectionalTool,
UltrasoundPleuraBLineTool,
PlanarFreehandROITool,
PlanarFreehandContourSegmentationTool,
SplineROITool,
@ -95,6 +96,7 @@ export default function initCornerstoneTools(configuration = {}) {
addTool(ImageOverlayViewerTool);
addTool(AdvancedMagnifyTool);
addTool(UltrasoundDirectionalTool);
addTool(UltrasoundPleuraBLineTool);
addTool(PlanarFreehandROITool);
addTool(SplineROITool);
addTool(LivewireContourTool);
@ -151,6 +153,7 @@ const toolNames = {
ImageOverlayViewer: ImageOverlayViewerTool.toolName,
AdvancedMagnify: AdvancedMagnifyTool.toolName,
UltrasoundDirectional: UltrasoundDirectionalTool.toolName,
UltrasoundAnnotation: UltrasoundPleuraBLineTool.toolName,
SplineROI: SplineROITool.toolName,
LivewireContour: LivewireContourTool.toolName,
PlanarFreehandROI: PlanarFreehandROITool.toolName,

View File

@ -38,6 +38,7 @@ const initMeasurementService = (
LivewireContour,
Probe,
UltrasoundDirectional,
UltrasoundPleuraBLine,
SegmentBidirectional,
} = measurementServiceMappingsFactory(
measurementService,
@ -177,6 +178,14 @@ const initMeasurementService = (
UltrasoundDirectional.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'UltrasoundPleuraBLineTool',
UltrasoundPleuraBLine.matchingCriteria,
UltrasoundPleuraBLine.toAnnotation,
UltrasoundPleuraBLine.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'SegmentBidirectional',

View File

@ -0,0 +1,179 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
import { getIsLocked } from './utils/getIsLocked';
import { getIsVisible } from './utils/getIsVisible';
import { annotation } from '@cornerstonejs/tools';
const UltrasoundPleuraBLine = {
toAnnotation: measurement => {},
/**
* Maps cornerstone annotation event data to measurement service format.
*
* @param {Object} cornerstone Cornerstone event data
* @return {Measurement} Measurement instance
*/
toMeasurement: (
csToolsEventDetail,
displaySetService,
CornerstoneViewportService,
getValueTypeFromToolType,
customizationService
) => {
const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
const isLocked = getIsLocked(annotationUID);
const isVisible = getIsVisible(annotationUID);
if (!metadata || !data) {
console.warn('UltrasoundPleuraBLine tool: Missing metadata or data');
return null;
}
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
const validToolType = SUPPORTED_TOOLS.includes(toolName);
if (!validToolType) {
throw new Error('Tool not supported');
}
const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } =
getSOPInstanceAttributes(referencedImageId);
let displaySet;
if (SOPInstanceUID) {
displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
const { points } = data.handles;
const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService);
return {
uid: annotationUID,
SOPInstanceUID,
FrameOfReferenceUID,
points,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
frameNumber: mappedAnnotations?.[0]?.frameNumber || 1,
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.label,
displayText: displayText,
data: data.cachedStats,
type: getValueTypeFromToolType(toolName),
getReport,
isLocked,
isVisible,
};
},
};
function getMappedAnnotations(annotation, DisplaySetService) {
const { metadata } = annotation;
const { annotationType } = annotation.data;
const { referencedImageId } = metadata;
const annotations = [];
if (!referencedImageId) {
throw new Error('Non-acquisition plane measurement mapping not supported');
}
const { SOPInstanceUID, SeriesInstanceUID, frameNumber } =
getSOPInstanceAttributes(referencedImageId);
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID,
frameNumber
);
const { SeriesNumber } = displaySet;
annotations.push({
SeriesInstanceUID,
SOPInstanceUID,
SeriesNumber,
frameNumber,
annotationType,
});
return annotations;
}
/*
This function is used to convert the measurement data to a format that is
suitable for the report generation (e.g. for the csv report). The report
returns a list of columns and corresponding values.
*/
function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService) {
const columns = [];
const values = [];
// Add Type
columns.push('AnnotationType');
values.push('Cornerstone:UltrasoundPleuraBLine');
mappedAnnotations.forEach(annotation => {
const { annotationType } = annotation;
columns.push('AnnotationType');
values.push(annotationType);
});
if (FrameOfReferenceUID) {
columns.push('FrameOfReferenceUID');
values.push(FrameOfReferenceUID);
}
if (points) {
columns.push('points');
values.push(points.map(p => p.join(' ')).join(';'));
}
return {
columns,
values,
};
}
function getDisplayText(mappedAnnotations, displaySet, customizationService) {
const displayText = {
primary: [],
secondary: [],
};
if (!mappedAnnotations || !mappedAnnotations.length) {
return displayText;
}
const { annotationType, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
let InstanceNumber;
if (instance) {
InstanceNumber = instance.InstanceNumber;
}
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
const seriesText = `S: ${SeriesNumber}${instanceText}${frameText}`;
displayText.primary.push(`Annotation : ${annotationType}`);
displayText.secondary.push(seriesText);
return displayText;
}
export default UltrasoundPleuraBLine;

View File

@ -12,6 +12,7 @@ const supportedTools = [
'SplineROI',
'LivewireContour',
'UltrasoundDirectionalTool',
'UltrasoundPleuraBLineTool',
'SCOORD3DPoint',
'SegmentBidirectional',
];

View File

@ -13,6 +13,7 @@ import LivewireContour from './LivewireContour';
import Probe from './Probe';
import UltrasoundDirectional from './UltrasoundDirectional';
import SegmentBidirectional from './SegmentBidirectional';
import UltrasoundPleuraBLine from './UltrasoundPleuraBLine';
const measurementServiceMappingsFactory = (
measurementService: MeasurementService,
@ -296,6 +297,23 @@ const measurementServiceMappingsFactory = (
},
],
},
UltrasoundPleuraBLine: {
toAnnotation: UltrasoundPleuraBLine.toAnnotation,
toMeasurement: csToolsAnnotation =>
UltrasoundPleuraBLine.toMeasurement(
csToolsAnnotation,
displaySetService,
cornerstoneViewportService,
_getValueTypeFromToolType,
customizationService
),
matchingCriteria: [
{
valueType: MeasurementService.VALUE_TYPES.POLYLINE,
points: 2,
},
],
},
};
return factories;

View File

@ -32,8 +32,8 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@cornerstonejs/core": "^3.15.6",
"@cornerstonejs/tools": "^3.15.6",
"@cornerstonejs/core": "^3.16.0",
"@cornerstonejs/tools": "^3.16.0",
"@ohif/core": "3.11.0-beta.60",
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.60",
"@ohif/extension-default": "3.11.0-beta.60",

104
extensions/usAnnotation/.gitignore vendored Normal file
View File

@ -0,0 +1,104 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port

View File

@ -0,0 +1,11 @@
{
"plugins": ["prettier-plugin-tailwindcss"],
"trailingComma": "es5",
"printWidth": 100,
"proseWrap": "always",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"arrowParens": "avoid",
"endOfLine": "auto"
}

View File

@ -0,0 +1,96 @@
const path = require('path');
const pkg = require('../package.json');
const outputFile = 'index.umd.js';
const rootDir = path.resolve(__dirname, '../');
const outputFolder = path.join(__dirname, `../dist/umd/${pkg.name}/`);
// Todo: add ESM build for the extension in addition to umd build
const config = {
mode: 'production',
entry: rootDir + '/' + pkg.module,
devtool: 'source-map',
output: {
path: outputFolder,
filename: outputFile,
library: pkg.name,
libraryTarget: 'umd',
chunkFilename: '[name].chunk.js',
umdNamedDefine: true,
globalObject: "typeof self !== 'undefined' ? self : this",
},
externals: [
{
react: {
root: 'React',
commonjs2: 'react',
commonjs: 'react',
amd: 'react',
},
'@ohif/core': {
commonjs2: '@ohif/core',
commonjs: '@ohif/core',
amd: '@ohif/core',
root: '@ohif/core',
},
'@ohif/ui': {
commonjs2: '@ohif/ui',
commonjs: '@ohif/ui',
amd: '@ohif/ui',
root: '@ohif/ui',
},
},
],
module: {
rules: [
{
test: /\.svg?$/,
oneOf: [
{
use: [
{
loader: '@svgr/webpack',
options: {
svgoConfig: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false
},
},
},
]
},
prettier: false,
svgo: true,
titleProp: true,
},
},
],
issuer: {
and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
},
},
],
},
{
test: /(\.jsx|\.js|\.tsx|\.ts)$/,
loader: 'babel-loader',
exclude: /(node_modules|bower_components)/,
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
],
},
resolve: {
modules: [path.resolve('./node_modules'), path.resolve('./src')],
extensions: ['.json', '.js', '.jsx', '.tsx', '.ts'],
},
};
module.exports = config;

View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2025 usAnnotation (rodrigo.basilio@radicalimaging.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,8 @@
# usAnnotation
## Description
## Author
OHIF
## License
MIT

View File

@ -0,0 +1,49 @@
module.exports = {
plugins: [
['@babel/plugin-proposal-class-properties', { loose: true }],
'@babel/plugin-transform-typescript',
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
['@babel/plugin-proposal-private-methods', { loose: true }],
],
env: {
test: {
presets: [
[
// TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2
'@babel/preset-env',
{
modules: 'commonjs',
debug: false,
},
],
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: [
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-regenerator',
'@babel/plugin-transform-runtime',
'@babel/plugin-transform-typescript',
],
},
production: {
presets: [
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
'@babel/preset-typescript',
],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
development: {
presets: [
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
'@babel/preset-typescript',
],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
},
};

View File

@ -0,0 +1,75 @@
{
"name": "@ohif/extension-ultrasound-pleura-bline",
"version": "3.0.0",
"description": "",
"author": "Rodrigo Basilio",
"license": "MIT",
"main": "dist/umd/usAnnotation/index.umd.js",
"files": [
"dist/**",
"public/**",
"README.md"
],
"repository": "OHIF/Viewers",
"keywords": [
"ohif-extension"
],
"module": "src/index.tsx",
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1.18.0"
},
"scripts": {
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:my-extension": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package": "yarn run build",
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "^3.11.0-beta.34",
"@ohif/extension-default": "^3.11.0-beta.34",
"@ohif/extension-cornerstone": "^3.11.0-beta.34",
"@ohif/i18n": "^1.0.0",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^12.2.2",
"react-router": "^6.23.1",
"react-router-dom": "^6.23.1",
"webpack": "5.89.0",
"webpack-merge": "^5.7.3"
},
"dependencies": {
"@babel/runtime": "^7.20.13"
},
"devDependencies": {
"@babel/core": "7.24.7",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-proposal-object-rest-spread": "^7.17.3",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-arrow-functions": "^7.16.7",
"@babel/plugin-transform-regenerator": "^7.16.7",
"@babel/plugin-transform-runtime": "7.24.7",
"@babel/plugin-transform-typescript": "^7.13.0",
"@babel/preset-env": "7.24.7",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.13.0",
"@babel/plugin-proposal-private-property-in-object": "7.21.11",
"babel-eslint": "9.x",
"babel-loader": "^8.2.4",
"@svgr/webpack": "^8.1.0",
"babel-plugin-module-resolver": "^5.0.0",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^10.2.0",
"cross-env": "^7.0.3",
"dotenv": "^14.1.0",
"eslint": "^8.39.0",
"eslint-loader": "^2.0.0",
"webpack": "5.89.0",
"webpack-merge": "^5.7.3",
"webpack-cli": "^5.0.2"
}
}

View File

@ -0,0 +1,10 @@
// Global state to control whether to show the percentage in the overlay
export let showPercentage = true;
/**
* Sets whether to show the pleura percentage in the viewport overlay
* @param value - Boolean indicating whether to show the percentage
*/
export function setShowPercentage(value) {
showPercentage = value;
}

View File

@ -0,0 +1,352 @@
import { UltrasoundPleuraBLineTool, Enums as csToolsEnums } from '@cornerstonejs/tools';
import { Types as OhifTypes } from '@ohif/core';
import { eventTarget, triggerEvent, utilities } from '@cornerstonejs/core';
import getInstanceByImageId from './getInstanceByImageId';
import { setShowPercentage } from './PleuraBlinePercentage';
const { transformWorldToIndex } = utilities;
/**
* Creates and returns the commands module for ultrasound annotation
* @param params - Extension parameters including servicesManager and commandsManager
* @returns The commands module with actions and definitions
*/
function commandsModule({
servicesManager,
commandsManager,
}: OhifTypes.Extensions.ExtensionParams): OhifTypes.Extensions.CommandsModule {
const { viewportGridService, toolGroupService, cornerstoneViewportService } =
servicesManager.services as AppTypes.Services;
const actions = {
/**
* Switches the active ultrasound annotation type
* @param options - Object containing the annotationType to switch to
*/
switchUSPleuraBLineAnnotation: ({ annotationType }) => {
const activeViewportId = viewportGridService.getActiveViewportId();
const toolGroup = toolGroupService.getToolGroupForViewport(activeViewportId);
if (!toolGroup) {
return;
}
const usAnnotation = toolGroup.getToolInstance(UltrasoundPleuraBLineTool.toolName);
if (usAnnotation) {
usAnnotation.setActiveAnnotationType(annotationType);
}
},
/**
* Convenience method to switch to pleura line annotation type
*/
switchUSPleuraBLineAnnotationToPleuraLine: () => {
actions.switchUSPleuraBLineAnnotation({
annotationType: UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.PLEURA,
});
},
/**
* Convenience method to switch to B-line annotation type
*/
switchUSPleuraBLineAnnotationToBLine: () => {
actions.switchUSPleuraBLineAnnotation({
annotationType: UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.BLINE,
});
},
/**
* Deletes the last annotation of the specified type
* @param options - Object containing the annotationType to delete
*/
deleteLastUSPleuraBLineAnnotation: ({ annotationType }) => {
const activeViewportId = viewportGridService.getActiveViewportId();
const toolGroup = toolGroupService.getToolGroupForViewport(activeViewportId);
if (!toolGroup) {
return;
}
const usAnnotation = toolGroup.getToolInstance(UltrasoundPleuraBLineTool.toolName);
if (usAnnotation) {
const viewport = cornerstoneViewportService.getCornerstoneViewport(activeViewportId);
usAnnotation.deleteLastAnnotationType(viewport.element, annotationType);
viewport.render();
}
},
/**
* Convenience method to delete the last pleura line annotation
*/
deleteLastPleuraAnnotation: () => {
actions.deleteLastUSPleuraBLineAnnotation({
annotationType: UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.PLEURA,
});
},
/**
* Convenience method to delete the last B-line annotation
*/
deleteLastBLineAnnotation: () => {
actions.deleteLastUSPleuraBLineAnnotation({
annotationType: UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.BLINE,
});
},
/**
* Toggles a boolean attribute of the ultrasound annotation tool
* @param options - Object containing the attribute name to toggle
*/
toggleUSToolAttribute: ({ attribute }) => {
const activeViewportId = viewportGridService.getActiveViewportId();
const toolGroup = toolGroupService.getToolGroupForViewport(activeViewportId);
if (!toolGroup) {
return;
}
const configuration = toolGroup.getToolConfiguration(UltrasoundPleuraBLineTool.toolName);
if (!configuration) {
return;
}
toolGroup.setToolConfiguration(UltrasoundPleuraBLineTool.toolName, {
[attribute]: !configuration[attribute],
});
const viewport = cornerstoneViewportService.getCornerstoneViewport(activeViewportId);
viewport.render();
},
/**
* Sets a specific attribute of the ultrasound annotation tool to a given value
* @param options - Object containing the attribute name and value to set
*/
setUSToolAttribute: ({ attribute, value }) => {
const activeViewportId = viewportGridService.getActiveViewportId();
const toolGroup = toolGroupService.getToolGroupForViewport(activeViewportId);
if (!toolGroup) {
return;
}
const configuration = toolGroup.getToolConfiguration(UltrasoundPleuraBLineTool.toolName);
if (!configuration) {
return;
}
toolGroup.setToolConfiguration(UltrasoundPleuraBLineTool.toolName, {
[attribute]: value,
});
const viewport = cornerstoneViewportService.getCornerstoneViewport(activeViewportId);
viewport.render();
},
/**
* Toggles the display of fan annotations
*/
toggleDisplayFanAnnotation: () => {
actions.toggleUSToolAttribute({
attribute: 'showFanAnnotations',
});
},
/**
* Toggles the display of the depth guide
*/
toggleDepthGuide: () => {
actions.toggleUSToolAttribute({
attribute: 'drawDepthGuide',
});
},
/**
* Sets the depth guide display state
* @param options - Object containing the boolean value to set
*/
setDepthGuide: ({ value }) => {
actions.setUSToolAttribute({
attribute: 'drawDepthGuide',
value,
});
},
/**
* Sets the fan annotation display state
* @param options - Object containing the boolean value to set
*/
setDisplayFanAnnotation: ({ value }) => {
actions.setUSToolAttribute({
attribute: 'showFanAnnotations',
value,
});
},
/**
* Sets whether to show the pleura percentage in the viewport overlay
* @param options - Object containing the boolean value to set
*/
setShowPleuraPercentage: ({ value }) => {
setShowPercentage(value);
// Trigger ANNOTATION_MODIFIED event to update the overlay
triggerEvent(eventTarget, csToolsEnums.Events.ANNOTATION_MODIFIED, {
annotation: {
metadata: {
toolName: UltrasoundPleuraBLineTool.toolName,
},
},
});
},
/**
* Generates a JSON representation of the ultrasound annotations
* @param labels - Array of annotation labels
* @param imageIds - Array of image IDs to include in the JSON
* @returns A JSON object containing the annotations data or undefined if generation fails
*/
generateUSPleuraBLineAnnotationsJSON: (labels: string[] = [], imageIds: string[] = []) => {
const activeViewportId = viewportGridService.getActiveViewportId();
const viewport = cornerstoneViewportService.getCornerstoneViewport(activeViewportId);
if (!viewport) {
return;
}
const { imageData } = viewport.getImageData() || {};
if (!imageData) {
return;
}
const toolGroup = toolGroupService.getToolGroupForViewport(activeViewportId);
if (!toolGroup) {
return;
}
const usAnnotation = toolGroup.getToolInstance(UltrasoundPleuraBLineTool.toolName);
if (usAnnotation) {
const configuration = toolGroup.getToolConfiguration(UltrasoundPleuraBLineTool.toolName);
const imageId = viewport.getCurrentImageId();
const filterImageIds = (imageId: string) => {
if (imageIds.length === 0) {
return true;
} else {
return imageIds.includes(imageId);
}
};
const annotations = UltrasoundPleuraBLineTool.filterAnnotations(
viewport.element,
filterImageIds
);
const frame_annotations = {};
const viewportImageIds = viewport.getImageIds();
annotations.forEach(annotation => {
const imageId = annotation.metadata.referencedImageId;
const { annotationType } = annotation.data;
const [point1, point2] = annotation.data.handles.points;
const p1 = transformWorldToIndex(imageData, point1);
const p2 = transformWorldToIndex(imageData, point2);
const imageIdIndex = viewportImageIds.indexOf(imageId);
if (frame_annotations[imageIdIndex] === undefined) {
frame_annotations[imageIdIndex] = {
pleura_lines: [],
b_lines: [],
};
}
if (annotationType === UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.PLEURA) {
frame_annotations[imageIdIndex].pleura_lines.push([
[p1[0], p1[1], 0],
[p2[0], p2[1], 0],
]);
} else if (
annotationType === UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.BLINE
) {
frame_annotations[imageIdIndex].b_lines.push([
[p1[0], p1[1], 0],
[p2[0], p2[1], 0],
]);
}
});
const instance = getInstanceByImageId(servicesManager.services, imageId);
const json = {
SOPInstanceUID: instance.SOPInstanceUID,
GrayscaleConversion: false,
mask_type: 'fan',
angle1: configuration.startAngle,
angle2: configuration.endAngle,
center_rows_px: configuration.center[0],
center_cols_px: configuration.center[1],
radius1: configuration.innerRadius,
radius2: configuration.outerRadius,
image_size_rows: instance.rows,
image_size_cols: instance.columns,
AnnotationLabels: labels,
frame_annotations,
};
return json;
}
},
/**
* Downloads the ultrasound annotations as a JSON file
* @param options - Object containing labels and imageIds arrays
*/
downloadUSPleuraBLineAnnotationsJSON({ labels = [], imageIds = [] }) {
const json = actions.generateUSPleuraBLineAnnotationsJSON(labels, imageIds);
if (!json) {
return;
}
// Convert JSON object to a string
const jsonString = JSON.stringify(json, null, 2);
// Create a blob with the JSON data
const blob = new Blob([jsonString], { type: 'application/json' });
// Create a URL for the blob
const url = URL.createObjectURL(blob);
// Create an anchor element
const a = document.createElement('a');
a.href = url;
a.download = `ultrasound_annotations_${new Date().toISOString().slice(0, 10)}.json`;
// Append to the document, click to download, and remove
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// Clean up by revoking the URL
URL.revokeObjectURL(url);
},
};
const definitions = {
switchUSAnnotation: {
commandFn: actions.switchUSPleuraBLineAnnotation,
},
deleteLastAnnotation: {
commandFn: actions.deleteLastUSPleuraBLineAnnotation,
},
toggleDepthGuide: {
commandFn: actions.toggleDepthGuide,
},
setDepthGuide: {
commandFn: actions.setDepthGuide,
},
setShowPleuraPercentage: {
commandFn: actions.setShowPleuraPercentage,
},
toggleUSToolAttribute: {
commandFn: actions.toggleUSToolAttribute,
},
setUSToolAttribute: {
commandFn: actions.setUSToolAttribute,
},
toggleDisplayFanAnnotation: {
commandFn: actions.toggleDisplayFanAnnotation,
},
setDisplayFanAnnotation: {
commandFn: actions.setDisplayFanAnnotation,
},
generateJSON: {
commandFn: actions.generateUSPleuraBLineAnnotationsJSON,
},
downloadJSON: {
commandFn: actions.downloadUSPleuraBLineAnnotationsJSON,
},
switchUSAnnotationToPleuraLine: {
commandFn: actions.switchUSPleuraBLineAnnotationToPleuraLine,
},
switchUSAnnotationToBLine: {
commandFn: actions.switchUSPleuraBLineAnnotationToBLine,
},
deleteLastPleuraAnnotation: {
commandFn: actions.deleteLastPleuraAnnotation,
},
deleteLastBLineAnnotation: {
commandFn: actions.deleteLastBLineAnnotation,
},
};
return {
actions,
definitions,
defaultContext: 'CORNERSTONE',
};
}
export default commandsModule;

View File

@ -0,0 +1,11 @@
/**
* Retrieves the DICOM instance associated with a specific imageId
* @param services - The OHIF services object
* @param imageId - The image ID to find the instance for
* @returns The DICOM instance object or undefined if not found
*/
export default function getInstanceByImageId(services: AppTypes.Services, imageId: string) {
const activeDisplaySets = services.displaySetService.getActiveDisplaySets();
const displaySet = activeDisplaySets.find(displaySet => displaySet?.imageIds?.includes(imageId));
return displaySet?.instance;
}

View File

@ -0,0 +1,31 @@
import React from 'react';
import USAnnotationPanel from './panels/USAnnotationPanel';
/**
* Creates and returns the panel module for ultrasound annotation
* @param params - Object containing commandsManager, servicesManager, and extensionManager
* @returns Array of panel configurations
*/
const getPanelModule = ({ commandsManager, servicesManager, extensionManager }: withAppTypes) => {
/**
* Wrapper component for the USAnnotationPanel that injects the required props
* @param props - Component props including configuration
* @returns The wrapped USAnnotationPanel component
*/
const wrappedUSAnnotationPanel = ({ configuration }) => {
return <USAnnotationPanel />;
};
return [
{
name: 'USAnnotationPanel',
iconName: 'tab-linear',
iconLabel: 'US Annotation',
label: 'USAnnotation',
component: wrappedUSAnnotationPanel,
},
];
};
export default getPanelModule;

View File

@ -0,0 +1,5 @@
import packageJson from '../package.json';
const id = packageJson.name;
export { id };

View File

@ -0,0 +1,90 @@
import { id } from './id';
import getPanelModule from './getPanelModule';
import getCommandsModule from './getCommandsModule';
import { showPercentage, setShowPercentage } from './PleuraBlinePercentage';
/**
* You can remove any of the following modules if you don't need them.
*/
export default {
/**
* Only required property. Should be a unique value across all extensions.
* You ID can be anything you want, but it should be unique.
*/
id,
/**
* Perform any pre-registration tasks here. This is called before the extension
* is registered. Usually we run tasks such as: configuring the libraries
* (e.g. cornerstone, cornerstoneTools, ...) or registering any services that
* this extension is providing.
*/
preRegistration: () => {},
/**
* PanelModule should provide a list of panels that will be available in OHIF
* for Modes to consume and render. Each panel is defined by a {name,
* iconName, iconLabel, label, component} object. Example of a panel module
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
*/
getPanelModule,
/**
* ViewportModule should provide a list of viewports that will be available in OHIF
* for Modes to consume and use in the viewports. Each viewport is defined by
* {name, component} object. Example of a viewport module is the CornerstoneViewport
* that is provided by the Cornerstone extension in OHIF.
*/
getViewportModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* ToolbarModule should provide a list of tool buttons that will be available in OHIF
* for Modes to consume and use in the toolbar. Each tool button is defined by
* {name, defaultComponent, clickHandler }. Examples include radioGroupIcons and
* splitButton toolButton that the default extension is providing.
*/
getToolbarModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* LayoutTemplateMOdule should provide a list of layout templates that will be
* available in OHIF for Modes to consume and use to layout the viewer.
* Each layout template is defined by a { name, id, component}. Examples include
* the default layout template provided by the default extension which renders
* a Header, left and right sidebars, and a viewport section in the middle
* of the viewer.
*/
getLayoutTemplateModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* SopClassHandlerModule should provide a list of sop class handlers that will be
* available in OHIF for Modes to consume and use to create displaySets from Series.
* Each sop class handler is defined by a { name, sopClassUids, getDisplaySetsFromSeries}.
* Examples include the default sop class handler provided by the default extension
*/
getSopClassHandlerModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* HangingProtocolModule should provide a list of hanging protocols that will be
* available in OHIF for Modes to use to decide on the structure of the viewports
* and also the series that hung in the viewports. Each hanging protocol is defined by
* { name, protocols}. Examples include the default hanging protocol provided by
* the default extension that shows 2x2 viewports.
*/
getHangingProtocolModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* CommandsModule should provide a list of commands that will be available in OHIF
* for Modes to consume and use in the viewports. Each command is defined by
* an object of { actions, definitions, defaultContext } where actions is an
* object of functions, definitions is an object of available commands, their
* options, and defaultContext is the default context for the command to run against.
*/
getCommandsModule,
/**
* ContextModule should provide a list of context that will be available in OHIF
* and will be provided to the Modes. A context is a state that is shared OHIF.
* Context is defined by an object of { name, context, provider }. Examples include
* the measurementTracking context provided by the measurementTracking extension.
*/
getContextModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* DataSourceModule should provide a list of data sources to be used in OHIF.
* DataSources can be used to map the external data formats to the OHIF's
* native format. DataSources are defined by an object of { name, type, createDataSource }.
*/
getDataSourcesModule: ({ servicesManager, commandsManager, extensionManager }) => {},
};
export { showPercentage, setShowPercentage };

View File

@ -0,0 +1,8 @@
import { metaData } from '@cornerstonejs/core';
import FanShapeGeometryProvider from '../providers/FanShapeGeometryProvider';
export default function init({ servicesManager }) {
const fanShapeGeometryProvider = new FanShapeGeometryProvider(servicesManager.services);
metaData.addProvider(fanShapeGeometryProvider.get.bind(fanShapeGeometryProvider), 9999);
}

View File

@ -0,0 +1,113 @@
import React, { useState, useEffect } from 'react';
import { Input } from '@ohif/ui-next';
/**
* Props for the MultiLabelInput component
*/
interface MultiLabelInputProps {
/** Placeholder text for the input field */
placeholder?: string;
/** Additional CSS class name for the input field */
className?: string;
/** Array of labels to display */
labels?: string[];
/** Callback function called when labels are added or removed */
onLabelsChange?: (labels: string[]) => void;
}
/**
* A component that allows users to input and manage multiple text labels
* @param props - Component props
* @returns The MultiLabelInput component
*/
const MultiLabelInput: React.FC<MultiLabelInputProps> = ({
placeholder = 'Enter a label and press Enter',
className,
labels = [],
onLabelsChange,
}) => {
// State for the current input value
const [inputValue, setInputValue] = useState<string>('');
// Internal state for labels that syncs with the external labels prop
const [internalLabels, setInternalLabels] = useState<string[]>(labels);
// Update internal labels when external labels change
useEffect(() => {
setInternalLabels(labels);
}, [labels]);
/**
* Handles changes to the input field
* @param e - The change event
*/
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
};
/**
* Adds the current input value as a new label if it's not empty and not a duplicate
*/
const handleAddLabel = () => {
if (inputValue.trim() && !internalLabels.includes(inputValue.trim())) {
const newLabels = [...internalLabels, inputValue.trim()];
setInternalLabels(newLabels);
if (onLabelsChange) {
onLabelsChange(newLabels);
}
setInputValue('');
}
};
/**
* Removes a label from the list
* @param label - The label to remove
*/
const handleRemoveLabel = (label: string) => {
const newLabels = internalLabels.filter(l => l !== label);
setInternalLabels(newLabels);
if (onLabelsChange) {
onLabelsChange(newLabels);
}
};
/**
* Handles key press events in the input field
* @param e - The keyboard event
*/
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleAddLabel();
}
};
return (
<div>
<Input
type="text"
value={inputValue}
onChange={handleInputChange}
onKeyPress={handleKeyPress}
placeholder={placeholder}
className={className}
/>
<div className="flex flex-wrap">
{internalLabels.map((label, index) => (
<span
key={index}
className="m-1 inline-flex items-center rounded-full bg-[#2a2a4a] px-4 py-1 text-white"
>
{label}
<button
className="ml-2.5 cursor-pointer rounded-full bg-blue-800 px-2 py-0.5 text-xs text-white transition-colors hover:bg-blue-600"
onClick={() => handleRemoveLabel(label)}
>
×
</button>
</span>
))}
</div>
</div>
);
};
export default MultiLabelInput;

View File

@ -0,0 +1,421 @@
import React, { useEffect, useState } from 'react';
import { Enums as csToolsEnums, UltrasoundPleuraBLineTool } from '@cornerstonejs/tools';
import { eventTarget, utilities } from '@cornerstonejs/core';
import { useSystem } from '@ohif/core';
import {
/* Layout */
PanelSection,
ScrollArea,
/* Controls */
Label,
Button,
Icons,
Switch,
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
Tabs,
TabsList,
TabsTrigger,
Separator,
} from '@ohif/ui-next';
/**
* A side panel that drives the ultrasound annotation workflow.
* It provides controls for managing annotations, toggling display options,
* and downloading annotations as JSON.
* @returns The USAnnotationPanel component
*/
export default function USAnnotationPanel() {
const { servicesManager, commandsManager } = useSystem();
/**
* Local state purely UI related (no business logic). */
const { viewportGridService, cornerstoneViewportService, measurementService } =
servicesManager.services as AppTypes.Services;
// UI state variables
const [depthGuide, setDepthGuide] = useState(true);
const [autoAdd, setAutoAdd] = useState(true);
const [showPleuraPct, setShowPleuraPct] = useState(true);
const [showOverlay, setShowOverlay] = useState(true);
// Data state variables
const [annotatedFrames, setAnnotatedFrames] = useState<any[]>([]);
const [imageIdsToObserve, setImageIdsToObserve] = useState<string[]>([]);
const [labels, setLabels] = useState<string[]>([]);
/**
* Helper commands bridging back to OHIF services. */
/**
* Switches the active annotation type (pleura or B-line)
* @param type - The annotation type to switch to
*/
const switchAnnotation = (type: string) => {
commandsManager.runCommand('setToolActive', { toolName: UltrasoundPleuraBLineTool.toolName });
commandsManager.runCommand('switchUSAnnotation', { annotationType: type });
};
/**
* Deletes the last annotation of the specified type
* @param type - The annotation type to delete
*/
const deleteLast = (type: string) => {
commandsManager.runCommand('deleteLastAnnotation', { annotationType: type });
updateAnnotatedFrames();
};
/**
* Sets the depth guide display state
* @param value - Boolean indicating whether to show the depth guide
*/
const setDepthGuideCommand = (value: boolean) => {
commandsManager.runCommand('setDepthGuide', { value });
setDepthGuide(value);
};
/**
* Sets the auto-add annotations state
* When enabled, all frames are monitored for annotations
* When disabled, only manually added frames are monitored
* @param value - Boolean indicating whether to auto-add annotations
*/
const setAutoAddCommand = (value: boolean) => {
if (value) {
setImageIdsToObserve([]);
} else {
const imageIds = annotatedFrames.map(item => item.imageId);
if (imageIds.length > 0) {
setImageIdsToObserve(imageIds);
} else {
setImageIdsToObserve(['Manual']);
}
}
setAutoAdd(value);
};
/**
* Sets whether to show the pleura percentage in the viewport overlay
* @param value - Boolean indicating whether to show the percentage
*/
const setShowPleuraPercentageCommand = (value: boolean) => {
commandsManager.runCommand('setShowPleuraPercentage', { value });
setShowPleuraPct(value);
};
/**
* Sets whether to show the fan overlay in the viewport
* @param value - Boolean indicating whether to show the overlay
*/
const setShowOverlayCommand = (value: boolean) => {
commandsManager.runCommand('setDisplayFanAnnotation', { value });
setShowOverlay(value);
};
/**
* Downloads the annotations as a JSON file
* Uses the labels and imageIdsToObserve state variables
*/
const downloadJSON = () => {
commandsManager.runCommand('downloadJSON', { labels, imageIds: imageIdsToObserve });
};
/**
* Adds the current image ID to the list of monitored image IDs
* Only works when auto-add is disabled
*/
const addCurrentImageId = () => {
if (!autoAdd) {
const activeViewportId = viewportGridService.getActiveViewportId();
const viewport = cornerstoneViewportService.getCornerstoneViewport(activeViewportId);
const currentImageId = viewport.getCurrentImageId();
const imageIds = [...imageIdsToObserve];
if (!imageIds.includes(currentImageId)) {
imageIds.push(currentImageId);
}
setImageIdsToObserve(imageIds);
}
};
/**
* Handles clicking on a row in the annotated frames table
* Scrolls the viewport to the selected frame
* @param item - The annotated frame item that was clicked
*/
const handleRowClick = item => {
const activeViewportId = viewportGridService.getActiveViewportId();
const viewport = cornerstoneViewportService.getCornerstoneViewport(activeViewportId);
utilities.scroll(viewport, { delta: item.frame - viewport.getCurrentImageIdIndex() });
};
/**
* Render helpers so the JSX doesnt become spaghetti. */
const renderWorkflowToggles = () => (
<PanelSection.Content>
<div className="space-y-3 p-2 text-sm text-white">
<div className="flex items-center">
<Switch
id="depth-guide-switch"
className="mr-3"
checked={depthGuide}
onCheckedChange={() => setDepthGuideCommand(!depthGuide)}
/>
<label
htmlFor="depth-guide-switch"
className="cursor-pointer"
onClick={() => setDepthGuideCommand(!depthGuide)}
>
Depth guide toggle
</label>
</div>
{/* <div className="flex items-center">
<Switch
id="auto-add-switch"
className="mr-3"
checked={autoAdd}
onCheckedChange={() => setAutoAddCommand(!autoAdd)}
/>
<label
htmlFor="auto-add-switch"
className="cursor-pointer"
onClick={() => setAutoAddCommand(!autoAdd)}
>
Auto-add annotations
</label>
</div> */}
<div className="flex items-center">
<Switch
id="pleura-percentage-switch"
className="mr-3"
checked={showPleuraPct}
onCheckedChange={() => setShowPleuraPercentageCommand(!showPleuraPct)}
/>
<label
htmlFor="pleura-percentage-switch"
className="cursor-pointer"
onClick={() => setShowPleuraPercentageCommand(!showPleuraPct)}
>
Show pleura percentage
</label>
</div>
</div>
</PanelSection.Content>
);
const renderSectorAnnotations = () => (
<PanelSection.Content>
<div className="flex flex-col gap-4 p-2">
<Label>Sector Annotations</Label>
<div className="flex items-center gap-2">
<Tabs
defaultValue={UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.BLINE}
onValueChange={newValue => switchAnnotation(newValue)}
>
<TabsList>
<TabsTrigger value={UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.PLEURA}>
<Icons.Plus /> Pleura line
</TabsTrigger>
<TabsTrigger value={UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.BLINE}>
<Icons.Plus /> B-line
</TabsTrigger>
<Separator orientation="vertical" />
<Separator orientation="vertical" />
</TabsList>
</Tabs>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="ml-auto">
<Icons.More />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
onClick={() =>
deleteLast(UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.BLINE)
}
>
<Icons.Delete className="text-foreground" />
<span className="pl-2">B-line annotation</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
deleteLast(UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.PLEURA)
}
>
<Icons.Delete className="text-foreground" />
<span className="pl-2">Pleura annotation</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="mt-2 flex items-center gap-2">
<Switch
id="show-overlay-switch"
checked={showOverlay}
onCheckedChange={() => setShowOverlayCommand(!showOverlay)}
className="data-[state=checked]:bg-blue-500"
/>
<label htmlFor="show-overlay-switch" className="cursor-pointer text-blue-300">
Show Overlay
</label>
</div>
{/* Divider */}
<hr className="border-t border-gray-800" />
</div>
</PanelSection.Content>
);
const renderAnnotatedFrames = () => (
<ScrollArea className="h-full">
<PanelSection.Content>
<div className="mb-4 flex items-center justify-between">
{/* <Button
variant="ghost"
size="sm"
className="text-blue-300"
disabled={autoAdd}
onClick={addCurrentImageId}
>
<Icons.Plus className="mr-2" /> Add current frame
</Button> */}
<Button variant="ghost" onClick={() => downloadJSON()}>
<Icons.Download className="h-5 w-5" />
<span>JSON</span>
</Button>
<Button variant="ghost" onClick={() => setShowOverlayCommand(!showOverlay)}>
{showOverlay ? <Icons.Hide className="h-5 w-5" /> : <Icons.Show className="h-5 w-5" />}
</Button>
</div>
<div className="w-full overflow-hidden">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b border-blue-900 text-blue-300">
<th></th>
<th className="py-2 px-3 text-left">Frame</th>
<th className="py-2 px-3 text-center">Pleura lines</th>
<th className="py-2 px-3 text-center">B-lines</th>
<th className="w-10"></th>
</tr>
</thead>
<tbody>
{annotatedFrames.map(item => (
<tr
key={item.frame}
className={`border-b border-blue-900 ${
item.frame === 5 ? 'bg-cyan-800 bg-opacity-30' : ''
}`}
onClick={() => handleRowClick(item)}
style={{ cursor: 'pointer' }}
>
<td className="py-3 px-3">{item.index}</td>
<td className="py-3 px-3">{item.frame + 1}</td>
<td className="py-3 px-3 text-center">{item.pleura}</td>
<td className="py-3 px-3 text-center">{item.bLine}</td>
<td className="py-3 px-3 text-right">
{item.frame === 5 && (
<div className="flex items-center justify-end">
<Button variant="ghost" className="p-0 text-blue-300">
<Icons.EyeVisible />
</Button>
<Button variant="ghost" className="ml-2 p-0 text-blue-300">
<Icons.More />
</Button>
</div>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</PanelSection.Content>
</ScrollArea>
);
const updateAnnotatedFrames = () => {
const activeViewportId = viewportGridService.getActiveViewportId();
const viewport = cornerstoneViewportService.getCornerstoneViewport(activeViewportId);
// copying to avoid mutating the original array
const imageIdsMonitored = [...imageIdsToObserve];
const imageIdFilter = (imageId: string) => {
if (imageIdsMonitored.length === 0) {
return true;
}
return imageIdsMonitored.includes(imageId);
};
const mapping = UltrasoundPleuraBLineTool.countAnnotations(viewport.element, imageIdFilter);
if (!mapping) {
setAnnotatedFrames([]);
return;
}
const keys = Array.from(mapping.keys());
const updatedFrames = keys.map((key, index) => {
const { pleura, bLine, frame } = mapping.get(key) || { pleura: 0, bLine: 0, frame: 0 };
return { imageId: key, index: index + 1, frame, pleura, bLine };
});
setAnnotatedFrames(updatedFrames);
};
/**
* Callback function that is called when an annotation is modified
* Updates the annotatedFrames state with the latest annotation data
*/
const annotationModified = React.useCallback(
event => {
if (event.detail.annotation.metadata.toolName === UltrasoundPleuraBLineTool.toolName) {
updateAnnotatedFrames();
}
},
[viewportGridService, cornerstoneViewportService, imageIdsToObserve]
);
useEffect(() => {
eventTarget.addEventListener(csToolsEnums.Events.ANNOTATION_MODIFIED, annotationModified);
const { unsubscribe } = measurementService.subscribe(
measurementService.EVENTS.MEASUREMENT_REMOVED,
() => {
updateAnnotatedFrames();
}
);
return () => {
eventTarget.removeEventListener(csToolsEnums.Events.ANNOTATION_MODIFIED, annotationModified);
unsubscribe();
};
}, [annotationModified, measurementService]);
/**
*
* 🖼 Final Render */
return (
<div className="h-full w-64 bg-black text-white">
{/* Workflow */}
<PanelSection>
<PanelSection.Header>Workflow</PanelSection.Header>
{renderWorkflowToggles()}
</PanelSection>
{/* Progress
<PanelSection>
<SectionHeader title="Workflow Progress" actionLabel="Source Folder" />
{renderWorkflowProgress()}
</PanelSection> */}
{/* Annotations */}
<PanelSection>
<PanelSection.Header> Annotations </PanelSection.Header>
{renderSectorAnnotations()}
</PanelSection>
{/* Annotated frames */}
<PanelSection className="flex-1">
<PanelSection.Header> Annotated Frames </PanelSection.Header>
{renderAnnotatedFrames()}
</PanelSection>
</div>
);
}

View File

@ -0,0 +1,28 @@
import getInstanceByImageId from '../getInstanceByImageId';
class FanShapeGeometryProvider {
services;
constructor(services) {
this.services = services;
}
get(query, imageId) {
if (query !== 'ultrasoundFanShapeGeometry') {
return null;
}
const instance = getInstanceByImageId(this.services, imageId);
// here you can add your logic to retrieve the fan shape geometry
// based on the instance or imageId
// The value returned should be an object with the following structure:
// return {
// center: Types.Point2; // The center of the fan shape in pixel coordinates (e.g. [-70, 80])
// startAngle: number; // The starting angle of the fan shape in degrees (e.g. 60)
// endAngle: number; // The ending angle of the fan shape in degrees (e.g. 120)
// innerRadius: number; // The inner radius of the fan shape (e.g. 300)
// outerRadius: number; // The outer radius of the fan shape (e.g. 650)
// };
return null;
}
}
export default FanShapeGeometryProvider;

104
modes/usAnnotation/.gitignore vendored Normal file
View File

@ -0,0 +1,104 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port

View File

@ -0,0 +1,11 @@
{
"plugins": ["prettier-plugin-tailwindcss"],
"trailingComma": "es5",
"printWidth": 100,
"proseWrap": "always",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"arrowParens": "avoid",
"endOfLine": "auto"
}

View File

@ -0,0 +1,100 @@
const path = require('path');
const pkg = require('../package.json');
const outputFile = 'index.umd.js';
const rootDir = path.resolve(__dirname, '../');
const outputFolder = path.join(__dirname, `../dist/umd/${pkg.name}/`);
// Todo: add ESM build for the mode in addition to umd build
const config = {
mode: 'production',
entry: rootDir + '/' + pkg.module,
devtool: 'source-map',
output: {
path: outputFolder,
filename: outputFile,
library: pkg.name,
libraryTarget: 'umd',
chunkFilename: '[name].chunk.js',
umdNamedDefine: true,
globalObject: "typeof self !== 'undefined' ? self : this",
},
externals: [
{
react: {
root: 'React',
commonjs2: 'react',
commonjs: 'react',
amd: 'react',
},
'@ohif/core': {
commonjs2: '@ohif/core',
commonjs: '@ohif/core',
amd: '@ohif/core',
root: '@ohif/core',
},
'@ohif/ui': {
commonjs2: '@ohif/ui',
commonjs: '@ohif/ui',
amd: '@ohif/ui',
root: '@ohif/ui',
},
'@ohif/mode-longitudinal': {
commonjs2: '@ohif/mode-longitudinal',
commonjs: '@ohif/mode-longitudinal',
amd: '@ohif/mode-longitudinal',
root: '@ohif/mode-longitudinal',
}
},
],
module: {
rules: [
{
test: /\.svg?$/,
oneOf: [
{
use: [
{
loader: '@svgr/webpack',
options: {
svgoConfig: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false
},
},
},
]
},
prettier: false,
svgo: true,
titleProp: true,
},
},
],
issuer: {
and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
},
},
],
},
{
test: /(\.jsx|\.js|\.tsx|\.ts)$/,
loader: 'babel-loader',
exclude: /(node_modules|bower_components)/,
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
],
},
resolve: {
modules: [path.resolve('./node_modules'), path.resolve('./src')],
extensions: ['.json', '.js', '.jsx', '.tsx', '.ts'],
},
};
module.exports = config;

View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2025 usAnnotation (rodrigo.basilio@radicalimaging.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,7 @@
# usAnnotation
## Description
## Author
OHIF
## License
MIT

View File

@ -0,0 +1,43 @@
module.exports = {
plugins: ['@babel/plugin-proposal-class-properties'],
env: {
test: {
presets: [
[
// TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2
'@babel/preset-env',
{
modules: 'commonjs',
debug: false,
},
'@babel/preset-typescript',
],
'@babel/preset-react',
],
plugins: [
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-regenerator',
'@babel/plugin-transform-runtime',
],
},
production: {
presets: [
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
'@babel/preset-typescript',
],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
development: {
presets: [
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
'@babel/preset-typescript',
],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
},
};

View File

@ -0,0 +1,64 @@
{
"name": "@ohif/mode-ultrasound-pleura-bline",
"version": "3.0.0",
"description": "Allows users to annotate ultrasound images with pleura B-line annotations.",
"author": "OHIF",
"license": "MIT",
"main": "dist/umd/usAnnotation/index.umd.js",
"files": [
"dist/**",
"public/**",
"README.md"
],
"repository": "OHIF/Viewers",
"keywords": [
"ohif-mode"
],
"module": "src/index.tsx",
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1.16.0"
},
"scripts": {
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:cornerstone": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package": "yarn run build",
"start": "yarn run dev",
"test:unit": "jest --watchAll",
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "^3.11.0-beta.34"
},
"dependencies": {
"@babel/runtime": "^7.20.13"
},
"devDependencies": {
"@babel/core": "7.24.7",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-proposal-object-rest-spread": "^7.17.3",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-arrow-functions": "^7.16.7",
"@babel/plugin-transform-regenerator": "^7.16.7",
"@babel/plugin-transform-runtime": "7.24.7",
"@babel/plugin-transform-typescript": "^7.13.0",
"@babel/preset-env": "7.24.7",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.13.0",
"babel-eslint": "^8.0.3",
"babel-loader": "^8.0.0-beta.4",
"@svgr/webpack": "^8.1.0",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^10.2.0",
"cross-env": "^7.0.3",
"dotenv": "^14.1.0",
"eslint": "^8.39.0",
"eslint-loader": "^2.0.0",
"webpack": "5.89.0",
"webpack-merge": "^5.7.3",
"webpack-cli": "^5.0.2"
}
}

View File

@ -0,0 +1,5 @@
import packageJson from '../package.json';
const id = packageJson.name;
export { id };

View File

@ -0,0 +1,391 @@
import i18n from 'i18next';
import { id } from './id';
import initToolGroups from './initToolGroups';
import toolbarButtons from './toolbarButtons';
import { UltrasoundPleuraBLineTool } from '@cornerstonejs/tools';
import { showPercentage } from '@ohif/extension-ultrasound-pleura-bline';
// Allow this mode by excluding non-imaging modalities such as SR, SEG
// Also, SM is not a simple imaging modalities, so exclude it.
const NON_IMAGE_MODALITIES = ['ECG', 'SEG', 'RTSTRUCT', 'RTPLAN', 'PR'];
const ohif = {
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
thumbnailList: '@ohif/extension-default.panelModule.seriesList',
wsiSopClassHandler:
'@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler',
};
const cornerstone = {
measurements: '@ohif/extension-cornerstone.panelModule.panelMeasurement',
segmentation: '@ohif/extension-cornerstone.panelModule.panelSegmentation',
};
const tracked = {
measurements: '@ohif/extension-measurement-tracking.panelModule.trackedMeasurements',
thumbnailList: '@ohif/extension-measurement-tracking.panelModule.seriesList',
viewport: '@ohif/extension-measurement-tracking.viewportModule.cornerstone-tracked',
};
const dicomsr = {
sopClassHandler: '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr',
sopClassHandler3D: '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr-3d',
viewport: '@ohif/extension-cornerstone-dicom-sr.viewportModule.dicom-sr',
};
const dicomvideo = {
sopClassHandler: '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video',
viewport: '@ohif/extension-dicom-video.viewportModule.dicom-video',
};
const dicompdf = {
sopClassHandler: '@ohif/extension-dicom-pdf.sopClassHandlerModule.dicom-pdf',
viewport: '@ohif/extension-dicom-pdf.viewportModule.dicom-pdf',
};
const dicomSeg = {
sopClassHandler: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg',
viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg',
};
const dicomPmap = {
sopClassHandler: '@ohif/extension-cornerstone-dicom-pmap.sopClassHandlerModule.dicom-pmap',
viewport: '@ohif/extension-cornerstone-dicom-pmap.viewportModule.dicom-pmap',
};
const dicomRT = {
viewport: '@ohif/extension-cornerstone-dicom-rt.viewportModule.dicom-rt',
sopClassHandler: '@ohif/extension-cornerstone-dicom-rt.sopClassHandlerModule.dicom-rt',
};
const usAnnotation = {
panel: '@ohif/extension-ultrasound-pleura-bline.panelModule.USAnnotationPanel',
};
let settingsSaved = {};
const extensionDependencies = {
// Can derive the versions at least process.env.from npm_package_version
'@ohif/extension-default': '^3.0.0',
'@ohif/extension-cornerstone': '^3.0.0',
'@ohif/extension-measurement-tracking': '^3.0.0',
'@ohif/extension-cornerstone-dicom-sr': '^3.0.0',
'@ohif/extension-cornerstone-dicom-seg': '^3.0.0',
'@ohif/extension-cornerstone-dicom-pmap': '^3.0.0',
'@ohif/extension-cornerstone-dicom-rt': '^3.0.0',
'@ohif/extension-dicom-pdf': '^3.0.1',
'@ohif/extension-dicom-video': '^3.0.1',
'@ohif/extension-ultrasound-pleura-bline': '^3.0.0',
};
function modeFactory({ modeConfiguration }) {
let _activatePanelTriggersSubscriptions = [];
return {
// TODO: We're using this as a route segment
// We should not be.
id,
routeName: 'usAnnotation',
displayName: i18n.t('US Pleura B-line Annotations'),
/**
* Lifecycle hooks
*/
onModeEnter: function ({
servicesManager,
extensionManager,
commandsManager,
appConfig,
}: withAppTypes) {
settingsSaved = {
disableConfirmationPrompts: appConfig?.disableConfirmationPrompts,
measurementTrackingMode: appConfig?.measurementTrackingMode,
};
appConfig.disableConfirmationPrompts = true;
appConfig.measurementTrackingMode = 'simplified';
const { measurementService, toolbarService, toolGroupService, customizationService } =
servicesManager.services;
measurementService.clearMeasurements();
// Init Default and SR ToolGroups
initToolGroups(extensionManager, toolGroupService, commandsManager);
toolbarService.register(toolbarButtons);
toolbarService.updateSection(toolbarService.sections.primary, [
'MeasurementTools',
'Zoom',
'Pan',
'TrackballRotate',
'WindowLevel',
'Capture',
'Layout',
'Crosshairs',
'MoreTools',
]);
toolbarService.updateSection(toolbarService.sections.viewportActionMenu.topLeft, [
'orientationMenu',
'dataOverlayMenu',
]);
toolbarService.updateSection(toolbarService.sections.viewportActionMenu.bottomMiddle, [
'AdvancedRenderingControls',
]);
toolbarService.updateSection(toolbarService.sections.advancedRenderingControlsSection, [
'windowLevelMenuEmbedded',
'voiManualControlMenu',
'Colorbar',
'opacityMenu',
'thresholdMenu',
]);
toolbarService.updateSection(toolbarService.sections.viewportActionMenu.topRight, [
'modalityLoadBadge',
'trackingStatus',
'navigationComponent',
]);
toolbarService.updateSection(toolbarService.sections.viewportActionMenu.bottomLeft, [
'windowLevelMenu',
]);
toolbarService.updateSection(toolbarService.sections.measurementSection, [
'Length',
'Bidirectional',
'ArrowAnnotate',
'EllipticalROI',
'RectangleROI',
'CircleROI',
'PlanarFreehandROI',
'SplineROI',
'LivewireContour',
]);
toolbarService.updateSection(toolbarService.sections.moreToolsSection, [
'Reset',
'rotate-right',
'flipHorizontal',
'ImageSliceSync',
'ReferenceLines',
'ImageOverlayViewer',
'StackScroll',
'invert',
'Probe',
'Cine',
'Angle',
'CobbAngle',
'Magnify',
'CalibrationLine',
'TagBrowser',
'AdvancedMagnify',
'UltrasoundDirectionalTool',
'UltrasoundPleuraBLineTool',
'WindowLevelRegion',
]);
customizationService.setCustomizations(
{
'panelSegmentation.disableEditing': {
$set: true,
},
autoCineModalities: {
$set: [],
},
'ohif.hotkeyBindings': {
$push: [
{
commandName: 'switchUSAnnotationToPleuraLine',
label: 'Add new pleura line',
keys: ['W'],
},
{
commandName: 'switchUSAnnotationToBLine',
label: 'Add new B-line',
keys: ['S'],
},
{
commandName: 'deleteLastPleuraAnnotation',
label: 'Delete last pleura line',
keys: ['E'],
},
{
commandName: 'deleteLastBLineAnnotation',
label: 'Delete last B-line',
keys: ['D'],
},
{
commandName: 'toggleDisplayFanAnnotation',
label: 'Toggle overlay',
keys: ['O'],
},
],
},
measurementsContextMenu: {
inheritsFrom: 'ohif.contextMenu',
menus: [
// Get the items from the UI Customization for the menu name (and have a custom name)
{
id: 'forExistingMeasurement',
selector: ({ nearbyToolData }) => !!nearbyToolData,
items: [
{
label: 'Delete annotation',
commands: 'removeMeasurement',
},
],
},
],
},
'viewportOverlay.topLeft': [
{
id: 'BLinePleuraPercentage',
inheritsFrom: 'ohif.overlayItem',
label: '',
title: 'BLinePleuraPercentage',
condition: ({ referenceInstance }) => referenceInstance?.Modality.includes('US'),
contentF: () => {
if (!showPercentage) {
return;
}
const { viewportGridService, toolGroupService, cornerstoneViewportService } =
servicesManager.services;
const activeViewportId = viewportGridService.getActiveViewportId();
const toolGroup = toolGroupService.getToolGroupForViewport(activeViewportId);
if (!toolGroup) {
return 'B-Line/Pleura : N/A';
}
const usAnnotation = toolGroup.getToolInstance(UltrasoundPleuraBLineTool.toolName);
if (usAnnotation) {
const viewport =
cornerstoneViewportService.getCornerstoneViewport(activeViewportId);
const percentage = usAnnotation.calculateBLinePleuraPercentage(viewport);
if (percentage !== undefined) {
return `B-Line/Pleura : ${percentage.toFixed(2)} %`;
} else {
return 'B-Line/Pleura : N/A';
}
}
return 'B-Line/Pleura : N/A';
},
},
],
},
'mode'
);
},
onModeExit: ({ servicesManager }: withAppTypes) => {
appConfig.disableConfirmationPrompts = settingsSaved.disableConfirmationPrompts;
appConfig.measurementTrackingMode = settingsSaved.measurementTrackingMode;
const {
toolGroupService,
syncGroupService,
segmentationService,
cornerstoneViewportService,
uiDialogService,
uiModalService,
} = servicesManager.services;
_activatePanelTriggersSubscriptions.forEach(sub => sub.unsubscribe());
_activatePanelTriggersSubscriptions = [];
uiDialogService.hideAll();
uiModalService.hide();
toolGroupService.destroy();
syncGroupService.destroy();
segmentationService.destroy();
cornerstoneViewportService.destroy();
},
validationTags: {
study: [],
series: [],
},
isValidMode: function ({ modalities }) {
const modalities_list = modalities.split('\\');
return {
valid: modalities_list.includes('US'),
description: 'Pleura b-lines annotation mode when the study involves US modality series',
};
},
routes: [
{
path: 'longitudinal',
/*init: ({ servicesManager, extensionManager }) => {
//defaultViewerRouteInit
},*/
layoutTemplate: () => {
return {
id: ohif.layout,
props: {
leftPanels: [tracked.thumbnailList],
leftPanelResizable: true,
rightPanels: [usAnnotation.panel, cornerstone.segmentation, tracked.measurements],
rightPanelResizable: true,
viewports: [
{
namespace: tracked.viewport,
displaySetsToDisplay: [
ohif.sopClassHandler,
dicomvideo.sopClassHandler,
dicomsr.sopClassHandler3D,
ohif.wsiSopClassHandler,
],
},
{
namespace: dicomsr.viewport,
displaySetsToDisplay: [dicomsr.sopClassHandler],
},
{
namespace: dicompdf.viewport,
displaySetsToDisplay: [dicompdf.sopClassHandler],
},
{
namespace: dicomSeg.viewport,
displaySetsToDisplay: [dicomSeg.sopClassHandler],
},
{
namespace: dicomPmap.viewport,
displaySetsToDisplay: [dicomPmap.sopClassHandler],
},
{
namespace: dicomRT.viewport,
displaySetsToDisplay: [dicomRT.sopClassHandler],
},
],
},
};
},
},
],
extensions: extensionDependencies,
// Default protocol gets self-registered by default in the init
hangingProtocol: 'default',
// Order is important in sop class handlers when two handlers both use
// the same sop class under different situations. In that case, the more
// general handler needs to come last. For this case, the dicomvideo must
// come first to remove video transfer syntax before ohif uses images
sopClassHandlers: [
dicomvideo.sopClassHandler,
dicomSeg.sopClassHandler,
dicomPmap.sopClassHandler,
ohif.sopClassHandler,
ohif.wsiSopClassHandler,
dicompdf.sopClassHandler,
dicomsr.sopClassHandler3D,
dicomsr.sopClassHandler,
dicomRT.sopClassHandler,
],
...modeConfiguration,
};
}
const mode = {
id,
modeFactory,
extensionDependencies,
};
export default mode;
export { initToolGroups, toolbarButtons };

View File

@ -0,0 +1,316 @@
import { toolNames as SRToolNames } from '@ohif/extension-cornerstone-dicom-sr';
const colours = {
'viewport-0': 'rgb(200, 0, 0)',
'viewport-1': 'rgb(200, 200, 0)',
'viewport-2': 'rgb(0, 200, 0)',
};
const colorsByOrientation = {
axial: 'rgb(200, 0, 0)',
sagittal: 'rgb(200, 200, 0)',
coronal: 'rgb(0, 200, 0)',
};
function initDefaultToolGroup(extensionManager, toolGroupService, commandsManager, toolGroupId) {
const utilityModule = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.tools'
);
const { toolNames, Enums } = utilityModule.exports;
const tools = {
active: [
{
toolName: toolNames.UltrasoundAnnotation,
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
},
{
toolName: toolNames.Pan,
bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }],
},
{
toolName: toolNames.Zoom,
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
},
{
toolName: toolNames.StackScroll,
bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
},
],
passive: [
{ toolName: toolNames.Length },
{
toolName: toolNames.ArrowAnnotate,
configuration: {
getTextCallback: (callback, eventDetails) => {
commandsManager.runCommand('arrowTextCallback', {
callback,
eventDetails,
});
},
changeTextCallback: (data, eventDetails, callback) => {
commandsManager.runCommand('arrowTextCallback', {
callback,
data,
eventDetails,
});
},
},
},
{
toolName: toolNames.SegmentBidirectional,
},
{ toolName: toolNames.Bidirectional },
{ toolName: toolNames.DragProbe },
{ toolName: toolNames.Probe },
{ toolName: toolNames.EllipticalROI },
{ toolName: toolNames.CircleROI },
{ toolName: toolNames.RectangleROI },
{ toolName: toolNames.StackScroll },
{ toolName: toolNames.Angle },
{ toolName: toolNames.CobbAngle },
{ toolName: toolNames.Magnify },
{ toolName: toolNames.CalibrationLine },
{
toolName: toolNames.PlanarFreehandContourSegmentation,
configuration: {
displayOnePointAsCrosshairs: true,
},
},
{ toolName: toolNames.UltrasoundDirectional },
{ toolName: toolNames.WindowLevel },
{ toolName: toolNames.PlanarFreehandROI },
{ toolName: toolNames.SplineROI },
{ toolName: toolNames.LivewireContour },
{ toolName: toolNames.WindowLevelRegion },
],
enabled: [
{ toolName: toolNames.ImageOverlayViewer },
{ toolName: toolNames.ReferenceLines },
{
toolName: SRToolNames.SRSCOORD3DPoint,
},
],
disabled: [
{
toolName: toolNames.AdvancedMagnify,
},
],
};
toolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
}
function initSRToolGroup(extensionManager, toolGroupService) {
const SRUtilityModule = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone-dicom-sr.utilityModule.tools'
);
if (!SRUtilityModule) {
return;
}
const CS3DUtilityModule = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.tools'
);
const { toolNames: SRToolNames } = SRUtilityModule.exports;
const { toolNames, Enums } = CS3DUtilityModule.exports;
const tools = {
active: [
{
toolName: toolNames.WindowLevel,
bindings: [
{
mouseButton: Enums.MouseBindings.Primary,
},
],
},
{
toolName: toolNames.Pan,
bindings: [
{
mouseButton: Enums.MouseBindings.Auxiliary,
},
],
},
{
toolName: toolNames.Zoom,
bindings: [
{
mouseButton: Enums.MouseBindings.Secondary,
},
],
},
{
toolName: toolNames.StackScroll,
bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
},
],
passive: [
{ toolName: SRToolNames.SRLength },
{ toolName: SRToolNames.SRArrowAnnotate },
{ toolName: SRToolNames.SRBidirectional },
{ toolName: SRToolNames.SREllipticalROI },
{ toolName: SRToolNames.SRCircleROI },
{ toolName: SRToolNames.SRPlanarFreehandROI },
{ toolName: SRToolNames.SRRectangleROI },
{ toolName: toolNames.WindowLevelRegion },
],
enabled: [
{
toolName: SRToolNames.DICOMSRDisplay,
},
],
// disabled
};
const toolGroupId = 'SRToolGroup';
toolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
}
function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) {
const utilityModule = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.tools'
);
const serviceManager = extensionManager._servicesManager;
const { cornerstoneViewportService } = serviceManager.services;
const { toolNames, Enums } = utilityModule.exports;
const tools = {
active: [
{
toolName: toolNames.WindowLevel,
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
},
{
toolName: toolNames.Pan,
bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }],
},
{
toolName: toolNames.Zoom,
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
},
{
toolName: toolNames.StackScroll,
bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
},
],
passive: [
{ toolName: toolNames.Length },
{
toolName: toolNames.ArrowAnnotate,
configuration: {
getTextCallback: (callback, eventDetails) => {
commandsManager.runCommand('arrowTextCallback', {
callback,
eventDetails,
});
},
changeTextCallback: (data, eventDetails, callback) => {
commandsManager.runCommand('arrowTextCallback', {
callback,
data,
eventDetails,
});
},
},
},
{ toolName: toolNames.Bidirectional },
{ toolName: toolNames.DragProbe },
{ toolName: toolNames.Probe },
{ toolName: toolNames.EllipticalROI },
{ toolName: toolNames.CircleROI },
{ toolName: toolNames.RectangleROI },
{ toolName: toolNames.StackScroll },
{ toolName: toolNames.Angle },
{ toolName: toolNames.CobbAngle },
{ toolName: toolNames.PlanarFreehandROI },
{ toolName: toolNames.SplineROI },
{ toolName: toolNames.LivewireContour },
{ toolName: toolNames.WindowLevelRegion },
{
toolName: toolNames.PlanarFreehandContourSegmentation,
configuration: {
displayOnePointAsCrosshairs: true,
},
},
],
disabled: [
{
toolName: toolNames.Crosshairs,
configuration: {
viewportIndicators: true,
viewportIndicatorsConfig: {
circleRadius: 5,
xOffset: 0.95,
yOffset: 0.05,
},
disableOnPassive: true,
autoPan: {
enabled: false,
panSize: 10,
},
getReferenceLineColor: viewportId => {
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
const viewportOptions = viewportInfo?.viewportOptions;
if (viewportOptions) {
return (
colours[viewportOptions.id] ||
colorsByOrientation[viewportOptions.orientation] ||
'#0c0'
);
} else {
console.warn('missing viewport?', viewportId);
return '#0c0';
}
},
},
},
{
toolName: toolNames.AdvancedMagnify,
},
{ toolName: toolNames.ReferenceLines },
],
};
toolGroupService.createToolGroupAndAddTools('mpr', tools);
}
function initVolume3DToolGroup(extensionManager, toolGroupService) {
const utilityModule = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.tools'
);
const { toolNames, Enums } = utilityModule.exports;
const tools = {
active: [
{
toolName: toolNames.TrackballRotateTool,
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
},
{
toolName: toolNames.Zoom,
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
},
{
toolName: toolNames.Pan,
bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }],
},
],
};
toolGroupService.createToolGroupAndAddTools('volume3d', tools);
}
function initToolGroups(extensionManager, toolGroupService, commandsManager) {
initDefaultToolGroup(extensionManager, toolGroupService, commandsManager, 'default');
initSRToolGroup(extensionManager, toolGroupService);
initMPRToolGroup(extensionManager, toolGroupService, commandsManager);
initVolume3DToolGroup(extensionManager, toolGroupService);
}
export default initToolGroups;

View File

@ -0,0 +1,703 @@
import type { Button } from '@ohif/core/types';
import { EVENTS } from '@cornerstonejs/core';
import { ViewportGridService } from '@ohif/core';
const callbacks = (toolName: string) => [
{
commandName: 'setViewportForToolConfiguration',
commandOptions: {
toolName,
},
},
];
export const setToolActiveToolbar = {
commandName: 'setToolActiveToolbar',
commandOptions: {
toolGroupIds: ['default', 'mpr', 'SRToolGroup', 'volume3d'],
},
};
const toolbarButtons: Button[] = [
// sections
{
id: 'MeasurementTools',
uiType: 'ohif.toolButtonList',
props: {
buttonSection: 'measurementSection',
},
},
{
id: 'MoreTools',
uiType: 'ohif.toolButtonList',
props: {
buttonSection: 'moreToolsSection',
},
},
{
id: 'AdvancedRenderingControls',
uiType: 'ohif.advancedRenderingControls',
props: {
buttonSection: 'advancedRenderingControlsSection',
},
},
// tool defs
{
id: 'modalityLoadBadge',
uiType: 'ohif.modalityLoadBadge',
props: {
icon: 'Status',
label: 'Status',
tooltip: 'Status',
evaluate: {
name: 'evaluate.modalityLoadBadge',
hideWhenDisabled: true,
},
},
},
{
id: 'navigationComponent',
uiType: 'ohif.navigationComponent',
props: {
icon: 'Navigation',
label: 'Navigation',
tooltip: 'Navigate between segments/measurements and manage their visibility',
evaluate: {
name: 'evaluate.navigationComponent',
hideWhenDisabled: true,
},
},
},
{
id: 'trackingStatus',
uiType: 'ohif.trackingStatus',
props: {
icon: 'TrackingStatus',
label: 'Tracking Status',
tooltip: 'View and manage tracking status of measurements and annotations',
evaluate: {
name: 'evaluate.trackingStatus',
hideWhenDisabled: true,
},
},
},
{
id: 'dataOverlayMenu',
uiType: 'ohif.dataOverlayMenu',
props: {
icon: 'ViewportViews',
label: 'Data Overlay',
tooltip: 'Configure data overlay options and manage foreground/background display sets',
evaluate: 'evaluate.dataOverlayMenu',
},
},
{
id: 'orientationMenu',
uiType: 'ohif.orientationMenu',
props: {
icon: 'OrientationSwitch',
label: 'Orientation',
tooltip:
'Change viewport orientation between axial, sagittal, coronal and acquisition planes',
evaluate: {
name: 'evaluate.orientationMenu',
// hideWhenDisabled: true,
},
},
},
{
id: 'windowLevelMenuEmbedded',
uiType: 'ohif.windowLevelMenuEmbedded',
props: {
icon: 'WindowLevel',
label: 'Window Level',
tooltip: 'Adjust window/level presets and customize image contrast settings',
evaluate: {
name: 'evaluate.windowLevelMenuEmbedded',
hideWhenDisabled: true,
},
},
},
{
id: 'windowLevelMenu',
uiType: 'ohif.windowLevelMenu',
props: {
icon: 'WindowLevel',
label: 'Window Level',
tooltip: 'Adjust window/level presets and customize image contrast settings',
evaluate: {
name: 'evaluate.windowLevelMenu',
},
},
},
{
id: 'voiManualControlMenu',
uiType: 'ohif.voiManualControlMenu',
props: {
icon: 'WindowLevelAdvanced',
label: 'Advanced Window Level',
tooltip: 'Advanced window/level settings with manual controls and presets',
evaluate: 'evaluate.voiManualControlMenu',
},
},
{
id: 'thresholdMenu',
uiType: 'ohif.thresholdMenu',
props: {
icon: 'Threshold',
label: 'Threshold',
tooltip: 'Image threshold settings',
evaluate: {
name: 'evaluate.thresholdMenu',
hideWhenDisabled: true,
},
},
},
{
id: 'opacityMenu',
uiType: 'ohif.opacityMenu',
props: {
icon: 'Opacity',
label: 'Opacity',
tooltip: 'Image opacity settings',
evaluate: {
name: 'evaluate.opacityMenu',
hideWhenDisabled: true,
},
},
},
{
id: 'Colorbar',
uiType: 'ohif.colorbar',
props: {
type: 'tool',
label: 'Colorbar',
},
},
{
id: 'Reset',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-reset',
label: 'Reset View',
tooltip: 'Reset View',
commands: 'resetViewport',
evaluate: 'evaluate.action',
},
},
{
id: 'rotate-right',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-rotate-right',
label: 'Rotate Right',
tooltip: 'Rotate +90',
commands: 'rotateViewportCW',
evaluate: [
'evaluate.action',
{
name: 'evaluate.viewport.supported',
unsupportedViewportTypes: ['video'],
},
],
},
},
{
id: 'flipHorizontal',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-flip-horizontal',
label: 'Flip Horizontal',
tooltip: 'Flip Horizontally',
commands: 'flipViewportHorizontal',
evaluate: [
'evaluate.viewportProperties.toggle',
{
name: 'evaluate.viewport.supported',
unsupportedViewportTypes: ['video', 'volume3d'],
},
],
},
},
{
id: 'ImageSliceSync',
uiType: 'ohif.toolButton',
props: {
icon: 'link',
label: 'Image Slice Sync',
tooltip: 'Enable position synchronization on stack viewports',
commands: {
commandName: 'toggleSynchronizer',
commandOptions: {
type: 'imageSlice',
},
},
listeners: {
[EVENTS.VIEWPORT_NEW_IMAGE_SET]: {
commandName: 'toggleImageSliceSync',
commandOptions: { toggledState: true },
},
},
evaluate: [
'evaluate.cornerstone.synchronizer',
{
name: 'evaluate.viewport.supported',
unsupportedViewportTypes: ['video', 'volume3d'],
},
],
},
},
{
id: 'ReferenceLines',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-referenceLines',
label: 'Reference Lines',
tooltip: 'Show Reference Lines',
commands: 'toggleEnabledDisabledToolbar',
listeners: {
[ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: callbacks('ReferenceLines'),
[ViewportGridService.EVENTS.VIEWPORTS_READY]: callbacks('ReferenceLines'),
},
evaluate: [
'evaluate.cornerstoneTool.toggle',
{
name: 'evaluate.viewport.supported',
unsupportedViewportTypes: ['video'],
},
],
},
},
{
id: 'ImageOverlayViewer',
uiType: 'ohif.toolButton',
props: {
icon: 'toggle-dicom-overlay',
label: 'Image Overlay',
tooltip: 'Toggle Image Overlay',
commands: 'toggleEnabledDisabledToolbar',
evaluate: [
'evaluate.cornerstoneTool.toggle',
{
name: 'evaluate.viewport.supported',
unsupportedViewportTypes: ['video'],
},
],
},
},
{
id: 'StackScroll',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-stack-scroll',
label: 'Stack Scroll',
tooltip: 'Stack Scroll',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
{
id: 'invert',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-invert',
label: 'Invert',
tooltip: 'Invert Colors',
commands: 'invertViewport',
evaluate: [
'evaluate.viewportProperties.toggle',
{
name: 'evaluate.viewport.supported',
unsupportedViewportTypes: ['video'],
},
],
},
},
{
id: 'Probe',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-probe',
label: 'Probe',
tooltip: 'Probe',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
{
id: 'Cine',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-cine',
label: 'Cine',
tooltip: 'Cine',
commands: 'toggleCine',
evaluate: [
'evaluate.cine',
{
name: 'evaluate.viewport.supported',
unsupportedViewportTypes: ['volume3d'],
},
],
},
},
{
id: 'Angle',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-angle',
label: 'Angle',
tooltip: 'Angle',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
{
id: 'CobbAngle',
uiType: 'ohif.toolButton',
props: {
icon: 'icon-tool-cobb-angle',
label: 'Cobb Angle',
tooltip: 'Cobb Angle',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
{
id: 'Magnify',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-magnify',
label: 'Zoom-in',
tooltip: 'Zoom-in',
commands: setToolActiveToolbar,
evaluate: [
'evaluate.cornerstoneTool',
{
name: 'evaluate.viewport.supported',
unsupportedViewportTypes: ['video'],
},
],
},
},
{
id: 'CalibrationLine',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-calibration',
label: 'Calibration',
tooltip: 'Calibration Line',
commands: setToolActiveToolbar,
evaluate: [
'evaluate.cornerstoneTool',
{
name: 'evaluate.viewport.supported',
unsupportedViewportTypes: ['video'],
},
],
},
},
{
id: 'TagBrowser',
uiType: 'ohif.toolButton',
props: {
icon: 'dicom-tag-browser',
label: 'Dicom Tag Browser',
tooltip: 'Dicom Tag Browser',
commands: 'openDICOMTagViewer',
},
},
{
id: 'AdvancedMagnify',
uiType: 'ohif.toolButton',
props: {
icon: 'icon-tool-loupe',
label: 'Magnify Probe',
tooltip: 'Magnify Probe',
commands: 'toggleActiveDisabledToolbar',
evaluate: [
'evaluate.cornerstoneTool.toggle.ifStrictlyDisabled',
{
name: 'evaluate.viewport.supported',
unsupportedViewportTypes: ['video'],
},
],
},
},
{
id: 'UltrasoundDirectionalTool',
uiType: 'ohif.toolButton',
props: {
icon: 'icon-tool-ultrasound-bidirectional',
label: 'Ultrasound Directional',
tooltip: 'Ultrasound Directional',
commands: setToolActiveToolbar,
evaluate: [
'evaluate.cornerstoneTool',
{
name: 'evaluate.modality.supported',
supportedModalities: ['US'],
},
],
},
},
{
id: 'UltrasoundPleuraBLineTool',
uiType: 'ohif.toolButton',
props: {
icon: 'icon-tool-ultrasound-bidirectional',
label: 'US Pleura B-line Annotation',
tooltip: 'US Pleura B-line Annotation',
commands: setToolActiveToolbar,
evaluate: [
'evaluate.cornerstoneTool',
{
name: 'evaluate.modality.supported',
supportedModalities: ['US'],
},
],
},
},
{
id: 'WindowLevelRegion',
uiType: 'ohif.toolButton',
props: {
icon: 'icon-tool-window-region',
label: 'Window Level Region',
tooltip: 'Window Level Region',
commands: setToolActiveToolbar,
evaluate: [
'evaluate.cornerstoneTool',
{
name: 'evaluate.viewport.supported',
unsupportedViewportTypes: ['video'],
},
],
},
},
{
id: 'Length',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-length',
label: 'Length',
tooltip: 'Length Tool',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
{
id: 'Bidirectional',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-bidirectional',
label: 'Bidirectional',
tooltip: 'Bidirectional Tool',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
{
id: 'ArrowAnnotate',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-annotate',
label: 'Annotation',
tooltip: 'Arrow Annotate',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
{
id: 'EllipticalROI',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-ellipse',
label: 'Ellipse',
tooltip: 'Ellipse ROI',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
{
id: 'RectangleROI',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-rectangle',
label: 'Rectangle',
tooltip: 'Rectangle ROI',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
{
id: 'CircleROI',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-circle',
label: 'Circle',
tooltip: 'Circle Tool',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
{
id: 'PlanarFreehandROI',
uiType: 'ohif.toolButton',
props: {
icon: 'icon-tool-freehand-roi',
label: 'Freehand ROI',
tooltip: 'Freehand ROI',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
{
id: 'SplineROI',
uiType: 'ohif.toolButton',
props: {
icon: 'icon-tool-spline-roi',
label: 'Spline ROI',
tooltip: 'Spline ROI',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
{
id: 'LivewireContour',
uiType: 'ohif.toolButton',
props: {
icon: 'icon-tool-livewire',
label: 'Livewire tool',
tooltip: 'Livewire tool',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
// Window Level
{
id: 'WindowLevel',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-window-level',
label: 'Window Level',
commands: setToolActiveToolbar,
evaluate: [
'evaluate.cornerstoneTool',
{
name: 'evaluate.viewport.supported',
unsupportedViewportTypes: ['wholeSlide'],
},
],
},
},
{
id: 'Pan',
uiType: 'ohif.toolButton',
props: {
type: 'tool',
icon: 'tool-move',
label: 'Pan',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
{
id: 'Zoom',
uiType: 'ohif.toolButton',
props: {
type: 'tool',
icon: 'tool-zoom',
label: 'Zoom',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
{
id: 'TrackballRotate',
uiType: 'ohif.toolButton',
props: {
type: 'tool',
icon: 'tool-3d-rotate',
label: '3D Rotate',
commands: setToolActiveToolbar,
evaluate: {
name: 'evaluate.cornerstoneTool',
disabledText: 'Select a 3D viewport to enable this tool',
},
},
},
{
id: 'Capture',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-capture',
label: 'Capture',
commands: 'showDownloadViewportModal',
evaluate: [
'evaluate.action',
{
name: 'evaluate.viewport.supported',
unsupportedViewportTypes: ['video', 'wholeSlide'],
},
],
},
},
{
id: 'Layout',
uiType: 'ohif.layoutSelector',
props: {
rows: 3,
columns: 4,
evaluate: 'evaluate.action',
},
},
{
id: 'Crosshairs',
uiType: 'ohif.toolButton',
props: {
type: 'tool',
icon: 'tool-crosshair',
label: 'Crosshairs',
commands: {
commandName: 'setToolActiveToolbar',
commandOptions: {
toolGroupIds: ['mpr'],
},
},
evaluate: {
name: 'evaluate.cornerstoneTool',
disabledText: 'Select an MPR viewport to enable this tool',
},
},
},
// {
// id: 'Undo',
// uiType: 'ohif.toolButton',
// props: {
// type: 'tool',
// icon: 'prev-arrow',
// label: 'Undo',
// commands: {
// commandName: 'undo',
// },
// evaluate: 'evaluate.action',
// },
// },
// {
// id: 'Redo',
// uiType: 'ohif.toolButton',
// props: {
// type: 'tool',
// icon: 'next-arrow',
// label: 'Redo',
// commands: {
// commandName: 'redo',
// },
// evaluate: 'evaluate.action',
// },
// },
];
export default toolbarButtons;

View File

@ -53,7 +53,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.15.6",
"@cornerstonejs/dicom-image-loader": "^3.16.0",
"@emotion/serialize": "^1.1.3",
"@ohif/core": "3.11.0-beta.60",
"@ohif/extension-cornerstone": "3.11.0-beta.60",

View File

@ -61,6 +61,10 @@
"packageName": "@ohif/extension-cornerstone-dicom-rt",
"default": false,
"version": "3.0.0"
},
{
"packageName": "@ohif/extension-ultrasound-pleura-bline",
"version": "3.0.0"
}
],
"modes": [
@ -88,6 +92,10 @@
"packageName": "@ohif/mode-basic-dev-mode",
"default": false,
"version": "3.0.0"
},
{
"packageName": "@ohif/mode-ultrasound-pleura-bline",
"version": "3.0.0"
}
],
"public": [

View File

@ -37,8 +37,8 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/core": "^3.15.6",
"@cornerstonejs/dicom-image-loader": "^3.15.6",
"@cornerstonejs/core": "^3.16.0",
"@cornerstonejs/dicom-image-loader": "^3.16.0",
"@ohif/ui": "3.11.0-beta.60",
"cornerstone-math": "0.1.9",
"dicom-parser": "^1.8.21"

6479
yarn.lock

File diff suppressed because it is too large Load Diff