Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

LDCT OHIF API — Integration Guide

Attribution

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.

Pipeline Overview

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.

LDCT simulation pipeline


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.


Step 1 — Copy the new form component

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.


Step 2 — Edit commandsModule.ts

File: Viewers/extensions/cornerstone/src/commandsModule.ts

Make 3 edits to this file:

Edit 2a — Add the import (around line 47)

Find this existing import:

import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';

Add these two lines directly after it:

import LDCTSimulationForm, {
  LDCTSimulationParams,
} from './utils/LDCTSimulationForm';

Edit 2b — Add the action function (inside the actions object)

Find this block (near the end of the actions object, look for showDownloadViewportModal):

    },
    /**
     * Toggle the reference lines tool

Or 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',
        });
      }
    },

Edit 2c — Register the command (inside the commands object)

Find this block near the bottom of the file:

    showDownloadViewportModal: {
      commandFn: actions.showDownloadViewportModal,
    },

Add directly after it:

    LDCT_AlsaihatiCommand: {
      commandFn: actions.LDCT_AlsaihatiCommand,
    },

Step 3 — Add toolbarButtons.ts

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 };

Step 4 — Edit index.ts

File: Viewers/modes/longitudinal/src/index.ts

Make 4 edits to this file:

Edit 4a — Import toolbarButtons (top of file)

Add this import near the top:

import { toolbarButtons } from './toolbarButtons';

Edit 4b — Import toolbarSections from @ohif/mode-basic

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';

Edit 4c — Define custom toolbarSections (after the tracked block)

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 TestButtonGroup later, or rename TestButtonGroup to anything — just keep it consistent with toolbarButtons.ts.

Edit 4d — Pass toolbarSections and toolbarButtons into modeInstance

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
};

Step 5 — Edit docker-compose.yml

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]

Step 6 — Fill in your paths in LDCT_OHIF_API/

Each file below already has a TODO comment marking exactly where to substitute your values.

LDCT_OHIF_API/config/base_storage.py (line 5)

Replace /path/to/your/storage with the directory where you want outputs saved.

LDCT_OHIF_API/services/ldct/ldct/blueprint.py (~line 570)

Replace YOUR_SERVER_IP with your dcm4chee server IP address.

LDCT_OHIF_API/services/ldct/ldct/steps/step8_4_upload_dicomweb.py (~line 37)

Replace YOUR_SERVER_IP with your dcm4chee server IP address.

LDCT_OHIF_API/services/ldct_flat/ldct/blueprint.py (~line 570)

Replace YOUR_SERVER_IP with your dcm4chee server IP address.

LDCT_OHIF_API/services/ldct_flat/ldct/steps/step8_4_upload_dicomweb.py (~line 37)

Replace YOUR_SERVER_IP with your dcm4chee server IP address.

LDCT_OHIF_API/monitor_ldct.sh (lines 17, 24, 35, 73)

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.


Step 7 — Create the storage directory

mkdir -p /your/storage/path/ldct_alsaihati

Step 8 — Build and run

cd Viewers/platform/app/.recipes/Nginx-Dcm4chee
docker-compose up --build

Summary of all files touched

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

Verify it works

  1. Open the OHIF viewer in your browser (http://YOUR_SERVER_IP)
  2. Load any CT study
  3. Look for the LDCT Alsaihati button in the toolbar
  4. Click it — a form popup should appear showing the Study UID and Series UID
  5. Click Start Simulation
  6. Watch for the notification toasts — started → completed
  7. The simulated series should appear in the same study in OHIF

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages