The underlying noise-insertion simulation (the idea and logic of estimating GNI/NPS from the input CT and using it to synthesize and scale reduced-dose noise) is from:
Alsaihati, N., Solomon, J., McCrum, E., & Samei, E. (2024). Development, validation, and application of a generic image‑based noise addition method for simulating reduced dose computed tomography images. Medical Physics. https://doi.org/10.1002/mp.17444
This repository serves mainly as an independent reimplementation of that method — the original authors' code is not publicly available, so this was built from the paper's description, with some of our own implementation decisions and adaptations. Whether the adaptations and implementation decicions reflect an ambiguity in their description, an implementation choice, or something specific to this dataset remains an open question.
Per-step detail on where this implementation follows the paper closely and where it deviates is documented in services/ldct/README.md.
The diagram below (workflow_schema.PNG) is our own diagram, not from the paper. It maps the pipeline scripts (Step 0 – Step 8.4) to how the reimplementation runs end-to-end and integrates into the OHIF viewer.
This guide assumes you have:
- Cloned the OHIF
Viewers/repo fresh from GitHub - Cloned
LDCT_OHIF_API/from GitLab onto the same server
You need to make 5 changes to Viewers/ and 3 config changes across both repos.
This file does not exist in the base OHIF repo. Copy it in from LDCT_OHIF_API/ or from the modified Viewers:
Copy to:
Viewers/extensions/cornerstone/src/utils/LDCTSimulationForm.tsx
The file is a React popup form that collects simulation parameters (dose factor, beta, GPU settings, etc.) and sends them to the API.
File: Viewers/extensions/cornerstone/src/commandsModule.ts
Make 3 edits to this file:
Find this existing import:
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';Add these two lines directly after it:
import LDCTSimulationForm, {
LDCTSimulationParams,
} from './utils/LDCTSimulationForm';Find this block (near the end of the actions object, look for showDownloadViewportModal):
},
/**
* Toggle the reference lines toolOr any closing block just before the actions object ends. Add the following function as the last entry in the actions object, before the closing };:
/**
* LDCT Simulation - Alsaihati Method
* Opens a modal to configure and run LDCT simulation
*/
LDCT_AlsaihatiCommand: ({ displaySetInstanceUID }: { displaySetInstanceUID?: string }) => {
let displaySet;
// If displaySetInstanceUID is not provided, get it from the active viewport
if (!displaySetInstanceUID) {
const activeViewportId = viewportGridService.getActiveViewportId();
if (!activeViewportId) {
uiNotificationService.show({
title: 'LDCT Simulation',
message: 'No active viewport found. Please select a series first.',
type: 'error',
duration: 5000,
});
return;
}
// Get display sets for the active viewport
const displaySets = cornerstoneViewportService.getViewportDisplaySets(activeViewportId);
if (!displaySets || displaySets.length === 0) {
uiNotificationService.show({
title: 'LDCT Simulation',
message: 'No display set found in active viewport. Please load a series first.',
type: 'error',
duration: 5000,
});
return;
}
// Use the first display set from the active viewport
displaySet = displaySets[0];
} else {
// Get the display set using the provided UID
displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
}
const studyUID = displaySet?.StudyInstanceUID || 'Not available';
const seriesUID = displaySet?.SeriesInstanceUID || 'Not available';
if (!displaySet || studyUID === 'Not available' || seriesUID === 'Not available') {
uiNotificationService.show({
title: 'LDCT Simulation',
message: 'Unable to get study/series information. Please select a series first.',
type: 'error',
duration: 5000,
});
return;
}
const { uiModalService } = servicesManager.services;
if (uiModalService) {
uiModalService.show({
content: LDCTSimulationForm,
title: 'LDCT Simulation - Alsaihati Method',
contentProps: {
studyUID,
seriesUID,
onSubmit: async (params: LDCTSimulationParams) => {
uiNotificationService.show({
title: 'LDCT Simulation',
message: `Starting LDCT simulation for series ${seriesUID}...`,
type: 'info',
duration: 5000,
});
try {
// !! Change this URL to your server IP !!
const response = await fetch('http://YOUR_SERVER_IP:8000/api/ldct/simulate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(params),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to start simulation');
}
uiNotificationService.show({
title: 'LDCT Simulation Started',
message: `Job ID: ${data.job_id}\nOutput: ${data.output_dir}`,
type: 'success',
duration: 10000,
});
// Poll for job status every 3 seconds
const pollInterval = setInterval(async () => {
try {
// !! Change this URL to your server IP !!
const statusResponse = await fetch(
`http://YOUR_SERVER_IP:8000/api/ldct/status/${data.job_id}`
);
const status = await statusResponse.json();
if (status.status === 'completed') {
clearInterval(pollInterval);
uiNotificationService.show({
title: 'LDCT Simulation Completed',
message: `Simulation completed!\nOutput: ${data.output_dir}`,
type: 'success',
duration: 10000,
});
} else if (status.status === 'failed') {
clearInterval(pollInterval);
uiNotificationService.show({
title: 'LDCT Simulation Failed',
message: `Error: ${status.error || 'Unknown error'}`,
type: 'error',
duration: 10000,
});
}
} catch (error) {
clearInterval(pollInterval);
console.error('Error polling job status:', error);
}
}, 3000);
} catch (error) {
uiNotificationService.show({
title: 'LDCT Simulation Error',
message: error instanceof Error ? error.message : 'Failed to start simulation',
type: 'error',
duration: 5000,
});
}
},
},
containerClassName: 'max-w-4xl p-4',
});
}
},Find this block near the bottom of the file:
showDownloadViewportModal: {
commandFn: actions.showDownloadViewportModal,
},Add directly after it:
LDCT_AlsaihatiCommand: {
commandFn: actions.LDCT_AlsaihatiCommand,
},File: Viewers/modes/longitudinal/src/toolbarButtons.ts
Define custom buttons. Add this button object to the buttons array:
import type { Button } from '@ohif/core/types';
import { toolbarButtons as basicToolbarButtons } from '@ohif/mode-basic';
// how to add dropdown list with buttons
// Define only your custom buttons
const customButtons: Button[] = [
// Define the section container
{
id: 'TestButtonGroup',
uiType: 'ohif.toolButtonList',
props: {
buttonSection: true,
},
},
// Define the individual buttons (without buttonSection property)
{
id: 'LDCT_Alsaihati',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-invert',
label: 'LDCT Alsaihati',
tooltip: 'LDCT Simulation using Alsaihati method',
commands: 'LDCT_AlsaihatiCommand',
evaluate: 'evaluate.action',
},
},
{
id: 'Option2',
uiType: 'ohif.toolButton',
props: {
icon: 'arrow-right',
label: 'Tool 2',
tooltip: 'Another option',
commands: 'someOtherCommand',
evaluate: 'evaluate.action',
},
},
{
id: 'Option3',
uiType: 'ohif.toolButton',
props: {
icon: 'arrow-down',
label: 'Tool 3',
tooltip: 'Third option',
commands: 'anotherCommand',
evaluate: 'evaluate.action',
},
},
];
// Combine basic buttons with your custom buttons
const toolbarButtons: Button[] = [
...basicToolbarButtons,
...customButtons,
];
export { toolbarButtons };
//how to add one button
// Define only your custom buttons
// const customButtons: Button[] = [
// {
// id: 'TestButton',
// uiType: 'ohif.toolButton',
// props: {
// icon: 'tool-capture',
// label: 'TEST',
// tooltip: 'This is a test button to verify toolbar configuration',
// commands: 'resetViewport',
// evaluate: 'evaluate.action',
// },
// },
// ];
// // Combine basic buttons with your custom buttons
// const toolbarButtons: Button[] = [
// ...basicToolbarButtons,
// ...customButtons,
// ];
// export { toolbarButtons };
File: Viewers/modes/longitudinal/src/index.ts
Make 4 edits to this file:
Add this import near the top:
import { toolbarButtons } from './toolbarButtons';In the existing import block from @ohif/mode-basic, add toolbarSections as basicToolbarSections:
import { initToolGroups, cornerstone,
ohif,
dicomsr,
dicomvideo,
basicLayout,
basicRoute,
extensionDependencies as basicDependencies,
mode as basicMode,
modeInstance as basicModeInstance,
toolbarSections as basicToolbarSections,
} from '@ohif/mode-basic';Add this block after the tracked(export const tracked = { ) constant:
export const toolbarSections = {
...basicToolbarSections,
primary: [
...basicToolbarSections.primary,
'TestButtonGroup',
],
TestButtonGroup: [
'LDCT_Alsaihati',
],
};You can add more button IDs to
TestButtonGrouplater, or renameTestButtonGroupto anything — just keep it consistent withtoolbarButtons.ts.
Find the modeInstance object and add both properties:
export const modeInstance = {
...basicModeInstance,
id,
routeName: 'viewer',
displayName: i18n.t('Modes:Basic Viewer'),
routes: [longitudinalRoute],
extensions: extensionDependencies,
toolbarSections, // ← add this
toolbarButtons, // ← add this
};File: Viewers/platform/app/.recipes/Nginx-Dcm4chee/docker-compose.yml
Add the ohif_api service at the bottom, before the networks: section:
ohif_api:
build:
context: /PATH/TO/LDCT_OHIF_API # !! change this to where you cloned LDCT_OHIF_API
dockerfile: Dockerfile
image: ohif_api:latest
container_name: ohif_api
ports:
- '8000:8000'
environment:
- FLASK_HOST=0.0.0.0
- FLASK_PORT=8000
- USE_GPU=True
- DEFAULT_GPU_INDEX=0
- CORS_ORIGINS=*
volumes:
- /PATH/TO/STORAGE:/PATH/TO/STORAGE # !! change this to your output storage dir
restart: on-failure
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]Each file below already has a TODO comment marking exactly where to substitute your values.
Replace /path/to/your/storage with the directory where you want outputs saved.
Replace YOUR_SERVER_IP with your dcm4chee server IP address.
Replace YOUR_SERVER_IP with your dcm4chee server IP address.
Replace YOUR_SERVER_IP with your dcm4chee server IP address.
Replace YOUR_SERVER_IP with your dcm4chee server IP address.
Replace YOUR_SERVER_IP with the IP of the server running the API, and /path/to/your/storage with the same storage path used in base_storage.py.
mkdir -p /your/storage/path/ldct_alsaihaticd Viewers/platform/app/.recipes/Nginx-Dcm4chee
docker-compose up --build| File | What you did |
|---|---|
extensions/cornerstone/src/utils/LDCTSimulationForm.tsx |
Copied in (new file) |
extensions/cornerstone/src/commandsModule.ts |
Added import + action function + command registration |
modes/longitudinal/src/toolbarButtons.ts |
Added toolbar button definition |
modes/longitudinal/src/index.ts |
Imported toolbarButtons + basicToolbarSections, defined toolbarSections with LDCT group, passed both into modeInstance |
platform/app/.recipes/Nginx-Dcm4chee/docker-compose.yml |
Added ohif_api service |
LDCT_OHIF_API/config/base_storage.py |
Updated storage path |
LDCT_OHIF_API/services/ldct/ldct/blueprint.py |
Updated dcm4chee server URL |
LDCT_OHIF_API/services/ldct/ldct/steps/step8_4_upload_dicomweb.py |
Updated dcm4chee server URL (fallback default) |
LDCT_OHIF_API/services/ldct_flat/ldct/blueprint.py |
Updated dcm4chee server URL |
LDCT_OHIF_API/services/ldct_flat/ldct/steps/step8_4_upload_dicomweb.py |
Updated dcm4chee server URL (fallback default) |
LDCT_OHIF_API/monitor_ldct.sh |
Updated API server hostname |
- Open the OHIF viewer in your browser (
http://YOUR_SERVER_IP) - Load any CT study
- Look for the LDCT Alsaihati button in the toolbar
- Click it — a form popup should appear showing the Study UID and Series UID
- Click Start Simulation
- Watch for the notification toasts — started → completed
- The simulated series should appear in the same study in OHIF