Instalasi OHIF Viewer PHTA #11

Open
opened 2025-05-27 12:00:52 +07:00 by mario · 0 comments
Owner

Date: Tue, 27 May 2025


Requirements:

  • OS Ubuntu Server 20+
  • RAM minimal 8GB jika ingin build Viewer di server
  • Docker Engine
  • Docker Compose

1. Install Docker

Set up Docker's apt repository

# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

Install the Docker packages (latest as possible)

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Tambahkan permission ke user saat ini (atau 'pacs' biasanya)

sudo usermod -aG docker $USER
newgrp docker

Verify:

sudo docker run hello-world

2. Run dicomweb-proxy:2.0

Login dan pull image:

docker login devone.aplikasi.web.id/gitea -u one
#password by WA
docker pull devone.aplikasi.web.id/one/dicomweb-proxy:2.0

#Verify:
docker images

Buat folder yang diperlukan:

mkdir -p /home/pacs/dicomweb-proxy
mkdir -p /home/pacs/dicomweb-proxy/config

Buat docker-compose.yml:

nano /home/pacs/dicomweb-proxy/docker-compose.yml

Lalu copykan script berikut:

version: '3'

services:
  dicomweb-proxy:
    image: devone.aplikasi.web.id/one/dicomweb-proxy:2.0
    container_name: dicomweb-proxy
    ports:
      - "5000:5000"
    volumes:
      - "/home/pacs/dicomweb-proxy/config/:/app/dicomweb-proxy/config/"
    restart: unless-stopped

Buat config default.json:

nano /home/pacs/dicomweb-proxy/config/default.json

Lalu copykan script berikut:

{
  "source": {
    "aet": "DICOMWEB_PROXY",
    "ip": "192.168.1.29",
    "port": 16888
  },

  "peers": [
    {
      "aet": "ABPACS",
      "ip": "192.168.1.29",
      "port": 11112
    }
  ],
  "transferSyntax": "1.2.840.10008.1.2.4.80",
  "mimeType": "image/dicom+jpeg",
  "lossyQuality": 60,
  "logDir": "./logs",
  "storagePath": "./data",
  "cacheRetentionMinutes": 60,
  "webserverPort": 5000,
  "useCget": true,
  "useFetchLevel": "SERIES",
  "maxAssociations": 4,
  "qidoMinChars": 0,
  "qidoAppendWildcard": true,
  "verboseLogging": false,
  "fullMeta": true,
  "websocketUrl": "",
  "websocketToken":
    "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
}

Config Adjustment

  • source.aet: buat AET baru untuk diregister ke dcm4chee, e.g: DICOMWEB_PROXY
  • source.ip : ubah jadi ip pacs server
  • source.port: ubah jadi port pacs server yang belum terpakai (bebas)
  • peers.aet: AET pacs server utama, biasanya 'ABPACS'
  • peers.ip: IP pacs server utama, biasanya ip lokal pacs server utama
  • peers.port: port AET pacs server utama, biasanya 11112

Run dan Verify

cd /home/pacs/dicomweb-proxy
docker-compose up -d

Verifikasi:
Buka ip.pacs.server:5000 di browser. Jika sudah muncul study list OHIF, maka sudah berhasil.

Troubleshoot

Untuk melihat log/troubleshoot jika ada eror:

docker ps -a
docker logs dicomweb-proxy

3. Install node.js >18.0

Install NVM

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

source ~/.bashrc
nvm install v20
nvm use v20

Verify:

which node

Pastikan hasil nya adalah node yang digunakan dari nvm. Misal:

/home/pacs/.nvm/versions/node/v20.19.1/bin/node

Clone OHIF

cd /home/pacs
git clone --branch prod-ab --single-branch https://devone.id/gitea/mario/ohif-viewer.git ohif-viewer

Install dependencies:

cd /home/pacs/ohif-viewer

# Install yarn
npm install --global yarn

yarn config set workspaces-experimental true
yarn install

Ubah config OHIF

Ubah config di platform/app/public/config/default.js:

// Di platform/app/public/config/default.js
// Cari dan sesuaikan field di bawah ini
expertise_host : http://ip.pacs.server
pacs_document_host: `192.168.1.29`, // IP ke NV di PACS Server untuk ambil pdf
pacs_document_port: 8080, // Port dcm4chee. Biasanya 8080
..

dataSources: [
    {
      namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
      sourceName: 'local-proxy',
      configuration: {
        friendlyName: 'Static WADO Local Data',
        name: 'DCM4CHEE',
        qidoRoot: `http://ip.dicomweb_proxy.pacs.server:port.proxy/rs`,
        wadoRoot: `http://ip.dicomweb_proxy.pacs.server:port.proxy/rs`,

Keterangan:

  • Jika dicomweb-proxy pada langkah sebelumnya dijalankan pada port 5000, maka qidoRoot dan wadoRoot diisi dengan ip.dicomweb_proxy.pacs.server:5000
  • expertise_host digunakan sebagai sumber mengambil expertise dari file nv/api.php maka biasanya ini diisi dengan IP PACS Server

Build OHIF

yarn run build

Jika ada eror TypeError: pathToRegExp.compile is not a function. Solusinya:
Modify /home/pacs/ohif-viewer/node_modules/serve-handler/src/index.js line 81

// From this
const toPath = pathToRegExp.compile(normalizedDest);

// To this
const toPath = (params) => normalizedDest.replace(/:([^\/]+)/g, (_, key) => params[key]);

Run OHIF

yarn global add http-server

cd /home/pacs/ohif-viewer/platform/app
npx serve ./dist -c ../public/serve.json

Lihat: ip.pacs.server:3000 untuk memastikan OHIF berjalan dengan normal

Allow OHIF Akses Expertise

Jangan lupa bypass CORS di nv/query.php agar OHIF bisa akses Expertise

// Add this at the very top of query.php, before any output or other code
header("Access-Control-Allow-Origin: http://{ip.server.abpacs}:{ip.ohif.default3000}");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With");
header("Access-Control-Allow-Credentials: true");

include_once("config.php");

4. Run Viewer as systemd

Jika sebelumnya viewer berjalan di atas terminal npx, yang mana akan mati ketika terminal ditutup. Maka solusinya adalah menggunakan systemd service. Berikut cara membuatnya:

  1. Bikin systemd .service file
sudo nano /etc/systemd/system/ohif-viewer.service

Isi dengan: sesuaikan path node

[Unit]
Description=OHIF Viewer Service
After=network.target

[Service]
Type=simple
User=pacs
Group=pacs
WorkingDirectory=/home/pacs/ohif-viewer/platform/app
ExecStart=/home/pacs/.nvm/.../npx serve ./dist -c ../public/serve.json
Restart=always
RestartSec=3
Environment=NODE_ENV=production
Environment=PATH=/home/pacs/.nvm/versions/node/v20.19.1/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
TimeoutStartSec=30
TimeoutStopSec=10
KillSignal=SIGINT

[Install]
WantedBy=multi-user.target

Periksa konfigurasi dan sesuaikan:

  • User dan Group : sesuai user server ini dijalankan. Biasanya 'pacs' dan 'pacs'
  • WorkingDirectory: letak codebase viewer. Default: /home/pacs/ohif-viewer/platform/app
  • ExecStart: pastikan direktori ohif-viewer/platform/app/dist dan file ohif-viewer/platform/app/public/serve.json ada di server
  • Environment: sesuaikan path dan versi node yang terinstall
  1. Enable dan Start Service
sudo systemctl daemon-reload
sudo systemctl enable ohif-viewer
sudo systemctl start ohif-viewer
sudo systemctl status ohif-viewer
  1. Buka ip.pacs.server:3000 di browser

  2. Sebisa mungkin hindari penggunaan Firefox karena akan ada bug 'Random Order Juggling' di Study List. Namun viewernya tidak masalah

Modifikasi OHIF

Jika ada modifikasi yang dilakukan di kode OHIF, setelah merubah, ulangi dari langkah Build OHIF untuk mengimplementasikan perubahan

5. (Alt) Jika Ubuntu18 Gagal Build OHIF

Jika Ubuntu 18 menyebabkan gagal build OHIF dan terlalu banyak perubahan yang beresiko. Maka gunakan OHIF Docker sebagai alternatif:

  1. Buat dir dan buat config
mkdir -p /home/pacs/ohif-docker
nano /home/pacs/ohif-docker/default.js

  1. Copy dan sesuaikan config default.js
/** @type {AppTypes.Config} */

window.config = {
  routerBasename: '/',
  // whiteLabeling: {},
  extensions: [],
  modes: [],
  customizationService: {},
  showStudyList: true,
  // some windows systems have issues with more than 3 web workers
  maxNumberOfWebWorkers: 3,
  // below flag is for performance reasons, but it might not work for all servers
  showWarningMessageForCrossOrigin: true,
  showCPUFallbackMessage: true,
  showLoadingIndicator: true,
  experimentalStudyBrowserSort: false,
  strictZSpacingForVolumeViewport: true,
  groupEnabledModesFirst: true,
  maxNumRequests: {
    interaction: 100,
    thumbnail: 75,
    // Prefetch number is dependent on the http protocol. For http 2 or
    // above, the number of requests can be go a lot higher.
    prefetch: 25,
  },
  expertise: false, //* Tambahan untuk enable expertise (CustomizableViewportOverlay)
  expertise_host: `http://152.42.173.210`, //* Tambahan untuk fetch data Expertise)
  pacs_document_host: `152.42.173.210`,
  pacs_document_port: 8080,
  // filterQueryParam: false,
  // defaultDataSourceName: 'dicomweb',
  defaultDataSourceName: 'local-proxy',
  dataSources: [
    {
      namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
      sourceName: 'local-proxy',
      configuration: {
        friendlyName: 'Static WADO Local Data',
        name: 'DCM4CHEE',
        qidoRoot: `http://152.42.173.210:5000/rs`,
        wadoRoot: `http://152.42.173.210:5000/rs`,
        qidoSupportsIncludeField: false,
        supportsReject: true,
        supportsStow: true,
        imageRendering: 'wadors',
        thumbnailRendering: 'wadors',
        enableStudyLazyLoad: true,
        supportsFuzzyMatching: false,
        supportsWildcard: true,
        staticWado: true,
        singlepart: 'video',
        bulkDataURI: {
          enabled: true,
          relativeResolution: 'studies',
        },
      },
    },

    {
      namespace: '@ohif/extension-default.dataSourcesModule.dicomwebproxy',
      sourceName: 'dicomwebproxy',
      configuration: {
        friendlyName: 'dicomweb delegating proxy',
        name: 'dicomwebproxy',
      },
    },
    {
      namespace: '@ohif/extension-default.dataSourcesModule.dicomjson',
      sourceName: 'dicomjson',
      configuration: {
        friendlyName: 'dicom json',
        name: 'json',
      },
    },
    {
      namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal',
      sourceName: 'dicomlocal',
      configuration: {
        friendlyName: 'dicom local',
      },
    },
  ],
  httpErrorHandler: error => {
    // This is 429 when rejected from the public idc sandbox too often.
    console.warn(error.status);

    // Could use services manager here to bring up a dialog/modal if needed.
    console.warn('test, navigate to https://ohif.org/');
  },
  hotkeys: [
    {
      commandName: 'incrementActiveViewport',
      label: 'Next Viewport',
      keys: ['right'],
    },
    {
      commandName: 'decrementActiveViewport',
      label: 'Previous Viewport',
      keys: ['left'],
    },
    { commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
    { commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
    { commandName: 'invertViewport', label: 'Invert', keys: ['i'] },
    {
      commandName: 'flipViewportHorizontal',
      label: 'Flip Horizontally',
      keys: ['h'],
    },
    {
      commandName: 'flipViewportVertical',
      label: 'Flip Vertically',
      keys: ['v'],
    },
    { commandName: 'scaleUpViewport', label: 'Zoom In', keys: ['+'] },
    { commandName: 'scaleDownViewport', label: 'Zoom Out', keys: ['-'] },
    { commandName: 'fitViewportToWindow', label: 'Zoom to Fit', keys: ['='] },
    { commandName: 'resetViewport', label: 'Reset', keys: ['space'] },
    { commandName: 'nextImage', label: 'Next Image', keys: ['down'] },
    { commandName: 'previousImage', label: 'Previous Image', keys: ['up'] },
    // {
    //   commandName: 'previousViewportDisplaySet',
    //   label: 'Previous Series',
    //   keys: ['pagedown'],
    // },
    // {
    //   commandName: 'nextViewportDisplaySet',
    //   label: 'Next Series',
    //   keys: ['pageup'],
    // },
    {
      commandName: 'setToolActive',
      commandOptions: { toolName: 'Zoom' },
      label: 'Zoom',
      keys: ['z'],
    },
    // ~ Window level presets
    {
      commandName: 'windowLevelPreset1',
      label: 'W/L Preset 1',
      keys: ['1'],
    },
    {
      commandName: 'windowLevelPreset2',
      label: 'W/L Preset 2',
      keys: ['2'],
    },
    {
      commandName: 'windowLevelPreset3',
      label: 'W/L Preset 3',
      keys: ['3'],
    },
    {
      commandName: 'windowLevelPreset4',
      label: 'W/L Preset 4',
      keys: ['4'],
    },
    {
      commandName: 'windowLevelPreset5',
      label: 'W/L Preset 5',
      keys: ['5'],
    },
    {
      commandName: 'windowLevelPreset6',
      label: 'W/L Preset 6',
      keys: ['6'],
    },
    {
      commandName: 'windowLevelPreset7',
      label: 'W/L Preset 7',
      keys: ['7'],
    },
    {
      commandName: 'windowLevelPreset8',
      label: 'W/L Preset 8',
      keys: ['8'],
    },
    {
      commandName: 'windowLevelPreset9',
      label: 'W/L Preset 9',
      keys: ['9'],
    },
  ],
  tours: [
    {
      id: 'basicViewerTour',
      route: '/viewer',
      steps: [
        {
          id: 'scroll',
          title: 'Scrolling Through Images',
          text: 'You can scroll through the images using the mouse wheel or scrollbar.',
          attachTo: {
            element: '.viewport-element',
            on: 'top',
          },
          advanceOn: {
            selector: '.cornerstone-viewport-element',
            event: 'CORNERSTONE_TOOLS_MOUSE_WHEEL',
          },
          beforeShowPromise: () => waitForElement('.viewport-element'),
        },
        {
          id: 'zoom',
          title: 'Zooming In and Out',
          text: 'You can zoom the images using the right click.',
          attachTo: {
            element: '.viewport-element',
            on: 'left',
          },
          advanceOn: {
            selector: '.cornerstone-viewport-element',
            event: 'CORNERSTONE_TOOLS_MOUSE_UP',
          },
          beforeShowPromise: () => waitForElement('.viewport-element'),
        },
        {
          id: 'pan',
          title: 'Panning the Image',
          text: 'You can pan the images using the middle click.',
          attachTo: {
            element: '.viewport-element',
            on: 'top',
          },
          advanceOn: {
            selector: '.cornerstone-viewport-element',
            event: 'CORNERSTONE_TOOLS_MOUSE_UP',
          },
          beforeShowPromise: () => waitForElement('.viewport-element'),
        },
        {
          id: 'windowing',
          title: 'Adjusting Window Level',
          text: 'You can modify the window level using the left click.',
          attachTo: {
            element: '.viewport-element',
            on: 'left',
          },
          advanceOn: {
            selector: '.cornerstone-viewport-element',
            event: 'CORNERSTONE_TOOLS_MOUSE_UP',
          },
          beforeShowPromise: () => waitForElement('.viewport-element'),
        },
        {
          id: 'length',
          title: 'Using the Measurement Tools',
          text: 'You can measure the length of a region using the Length tool.',
          attachTo: {
            element: '[data-cy="MeasurementTools-split-button-primary"]',
            on: 'bottom',
          },
          advanceOn: {
            selector: '[data-cy="MeasurementTools-split-button-primary"]',
            event: 'click',
          },
          beforeShowPromise: () =>
            waitForElement('[data-cy="MeasurementTools-split-button-primary]'),
        },
        {
          id: 'drawAnnotation',
          title: 'Drawing Length Annotations',
          text: 'Use the length tool on the viewport to measure the length of a region.',
          attachTo: {
            element: '.viewport-element',
            on: 'right',
          },
          advanceOn: {
            selector: 'body',
            event: 'event::measurement_added',
          },
          beforeShowPromise: () => waitForElement('.viewport-element'),
        },
        {
          id: 'trackMeasurement',
          title: 'Tracking Measurements in the Panel',
          text: 'Click yes to track the measurements in the measurement panel.',
          attachTo: {
            element: '[data-cy="prompt-begin-tracking-yes-btn"]',
            on: 'bottom',
          },
          advanceOn: {
            selector: '[data-cy="prompt-begin-tracking-yes-btn"]',
            event: 'click',
          },
          beforeShowPromise: () => waitForElement('[data-cy="prompt-begin-tracking-yes-btn"]'),
        },
        {
          id: 'openMeasurementPanel',
          title: 'Opening the Measurements Panel',
          text: 'Click the measurements button to open the measurements panel.',
          attachTo: {
            element: '#trackedMeasurements-btn',
            on: 'left-start',
          },
          advanceOn: {
            selector: '#trackedMeasurements-btn',
            event: 'click',
          },
          beforeShowPromise: () => waitForElement('#trackedMeasurements-btn'),
        },
        {
          id: 'scrollAwayFromMeasurement',
          title: 'Scrolling Away from a Measurement',
          text: 'Scroll the images using the mouse wheel away from the measurement.',
          attachTo: {
            element: '.viewport-element',
            on: 'left',
          },
          advanceOn: {
            selector: '.cornerstone-viewport-element',
            event: 'CORNERSTONE_TOOLS_MOUSE_WHEEL',
          },
          beforeShowPromise: () => waitForElement('.viewport-element'),
        },
        {
          id: 'jumpToMeasurement',
          title: 'Jumping to Measurements in the Panel',
          text: 'Click the measurement in the measurement panel to jump to it.',
          attachTo: {
            element: '[data-cy="data-row"]',
            on: 'left-start',
          },
          advanceOn: {
            selector: '[data-cy="data-row"]',
            event: 'click',
          },
          beforeShowPromise: () => waitForElement('[data-cy="data-row"]'),
        },
        {
          id: 'changeLayout',
          title: 'Changing Layout',
          text: 'You can change the layout of the viewer using the layout button.',
          attachTo: {
            element: '[data-cy="Layout"]',
            on: 'bottom',
          },
          advanceOn: {
            selector: '[data-cy="Layout"]',
            event: 'click',
          },
          beforeShowPromise: () => waitForElement('[data-cy="Layout"]'),
        },
        {
          id: 'selectLayout',
          title: 'Selecting the MPR Layout',
          text: 'Select the MPR layout to view the images in MPR mode.',
          attachTo: {
            element: '[data-cy="MPR"]',
            on: 'left-start',
          },
          advanceOn: {
            selector: '[data-cy="MPR"]',
            event: 'click',
          },
          beforeShowPromise: () => waitForElement('[data-cy="MPR"]'),
        },
      ],
      tourOptions: {
        useModalOverlay: true,
        defaultStepOptions: {
          buttons: [
            {
              text: 'Skip all',
              action() {
                this.complete();
              },
              secondary: true,
            },
          ],
        },
      },
    },
  ],
};

function waitForElement(selector, maxAttempts = 20, interval = 25) {
  return new Promise(resolve => {
    let attempts = 0;

    const checkForElement = setInterval(() => {
      const element = document.querySelector(selector);

      if (element || attempts >= maxAttempts) {
        clearInterval(checkForElement);
        resolve();
      }

      attempts++;
    }, interval);
  });
}

  1. Pull images
docker login devone.aplikasi.web.id/gitea -u one
docker pull devone.aplikasi.web.id/one/ohif-391:v2.0
  1. Run
docker run -d -p 3000:80/tcp -v /home/pacs/ohif-docker/default.js:/usr/share/nginx/html/app-config.js --name ohif-viewer-container devone.aplikasi.web.id/one/ohif-391:v2.0

Re run ohif-container

docker pull devone.aplikasi.web.id/one/ohif-391:{versi_image_baru misal v3.0}

docker stop ohif-viewer-container
docker rm ohif-viewer-container
docker run -d -p 3000:80/tcp -v /home/pacs/ohif-docker/default.js:/usr/share/nginx/html/app-config.js --name ohif-viewer-container devone.aplikasi.web.id/one/ohif-391:{versi_image_baru misal v3.0}
Date: Tue, 27 May 2025 --- **Requirements:** - OS Ubuntu Server 20+ - RAM minimal 8GB jika ingin build Viewer di server - Docker Engine - Docker Compose ## 1. Install Docker Set up Docker's `apt` repository ```bash # Add Docker's official GPG key: sudo apt-get update sudo apt-get install ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc # Add the repository to Apt sources: echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update ``` Install the Docker packages (latest as possible) ```sh sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin ``` Tambahkan permission ke user saat ini (atau 'pacs' biasanya) ```sh sudo usermod -aG docker $USER newgrp docker ``` Verify: ```sh sudo docker run hello-world ``` ## 2. Run dicomweb-proxy:2.0 #### Login dan pull image: ```sh docker login devone.aplikasi.web.id/gitea -u one #password by WA docker pull devone.aplikasi.web.id/one/dicomweb-proxy:2.0 #Verify: docker images ``` #### Buat folder yang diperlukan: ```sh mkdir -p /home/pacs/dicomweb-proxy mkdir -p /home/pacs/dicomweb-proxy/config ``` #### Buat `docker-compose.yml`: ```sh nano /home/pacs/dicomweb-proxy/docker-compose.yml ``` Lalu copykan script berikut: ```yml version: '3' services: dicomweb-proxy: image: devone.aplikasi.web.id/one/dicomweb-proxy:2.0 container_name: dicomweb-proxy ports: - "5000:5000" volumes: - "/home/pacs/dicomweb-proxy/config/:/app/dicomweb-proxy/config/" restart: unless-stopped ``` #### Buat config `default.json`: ``` nano /home/pacs/dicomweb-proxy/config/default.json ``` Lalu copykan script berikut: ```json { "source": { "aet": "DICOMWEB_PROXY", "ip": "192.168.1.29", "port": 16888 }, "peers": [ { "aet": "ABPACS", "ip": "192.168.1.29", "port": 11112 } ], "transferSyntax": "1.2.840.10008.1.2.4.80", "mimeType": "image/dicom+jpeg", "lossyQuality": 60, "logDir": "./logs", "storagePath": "./data", "cacheRetentionMinutes": 60, "webserverPort": 5000, "useCget": true, "useFetchLevel": "SERIES", "maxAssociations": 4, "qidoMinChars": 0, "qidoAppendWildcard": true, "verboseLogging": false, "fullMeta": true, "websocketUrl": "", "websocketToken": "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" } ``` #### Config Adjustment - source.aet: buat AET baru untuk diregister ke dcm4chee, e.g: DICOMWEB_PROXY - source.ip : ubah jadi ip pacs server - source.port: ubah jadi port pacs server yang belum terpakai (bebas) - peers.aet: AET pacs server utama, biasanya 'ABPACS' - peers.ip: IP pacs server utama, biasanya ip lokal pacs server utama - peers.port: port AET pacs server utama, biasanya 11112 #### Run dan Verify ```sh cd /home/pacs/dicomweb-proxy docker-compose up -d ``` Verifikasi: Buka `ip.pacs.server:5000` di browser. Jika sudah muncul study list OHIF, maka sudah berhasil. #### Troubleshoot Untuk melihat log/troubleshoot jika ada eror: ```sh docker ps -a docker logs dicomweb-proxy ``` ## 3. Install node.js >18.0 #### Install NVM ```sh curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" source ~/.bashrc nvm install v20 nvm use v20 ``` Verify: ```sh which node ``` Pastikan hasil nya adalah node yang digunakan dari nvm. Misal: ``` /home/pacs/.nvm/versions/node/v20.19.1/bin/node ``` #### Clone OHIF ```sh cd /home/pacs git clone --branch prod-ab --single-branch https://devone.id/gitea/mario/ohif-viewer.git ohif-viewer ``` Install dependencies: ```sh cd /home/pacs/ohif-viewer # Install yarn npm install --global yarn yarn config set workspaces-experimental true yarn install ``` #### Ubah config OHIF Ubah config di `platform/app/public/config/default.js`: ```js // Di platform/app/public/config/default.js // Cari dan sesuaikan field di bawah ini expertise_host : http://ip.pacs.server pacs_document_host: `192.168.1.29`, // IP ke NV di PACS Server untuk ambil pdf pacs_document_port: 8080, // Port dcm4chee. Biasanya 8080 .. dataSources: [ { namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', sourceName: 'local-proxy', configuration: { friendlyName: 'Static WADO Local Data', name: 'DCM4CHEE', qidoRoot: `http://ip.dicomweb_proxy.pacs.server:port.proxy/rs`, wadoRoot: `http://ip.dicomweb_proxy.pacs.server:port.proxy/rs`, ``` Keterangan: - Jika **dicomweb-proxy** pada langkah sebelumnya dijalankan pada port **5000**, maka **qidoRoot** dan **wadoRoot** diisi dengan `ip.dicomweb_proxy.pacs.server:5000` - **expertise_host** digunakan sebagai sumber mengambil expertise dari file `nv/api.php` maka biasanya ini diisi dengan IP PACS Server #### Build OHIF ```sh yarn run build ``` Jika ada eror `TypeError: pathToRegExp.compile is not a function`. Solusinya: Modify `/home/pacs/ohif-viewer/node_modules/serve-handler/src/index.js` line 81 ```js // From this const toPath = pathToRegExp.compile(normalizedDest); // To this const toPath = (params) => normalizedDest.replace(/:([^\/]+)/g, (_, key) => params[key]); ``` #### Run OHIF ```sh yarn global add http-server cd /home/pacs/ohif-viewer/platform/app npx serve ./dist -c ../public/serve.json ``` Lihat: `ip.pacs.server:3000` untuk memastikan OHIF berjalan dengan normal #### Allow OHIF Akses Expertise **Jangan lupa bypass CORS di nv/query.php agar OHIF bisa akses Expertise** ```php // Add this at the very top of query.php, before any output or other code header("Access-Control-Allow-Origin: http://{ip.server.abpacs}:{ip.ohif.default3000}"); header("Access-Control-Allow-Methods: GET, POST, OPTIONS"); header("Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With"); header("Access-Control-Allow-Credentials: true"); include_once("config.php"); ``` ## 4. Run Viewer as systemd Jika sebelumnya viewer berjalan di atas terminal npx, yang mana akan mati ketika terminal ditutup. Maka solusinya adalah menggunakan `systemd service`. Berikut cara membuatnya: 1. Bikin `systemd .service` file ```sh sudo nano /etc/systemd/system/ohif-viewer.service ``` Isi dengan: **sesuaikan path node** ``` [Unit] Description=OHIF Viewer Service After=network.target [Service] Type=simple User=pacs Group=pacs WorkingDirectory=/home/pacs/ohif-viewer/platform/app ExecStart=/home/pacs/.nvm/.../npx serve ./dist -c ../public/serve.json Restart=always RestartSec=3 Environment=NODE_ENV=production Environment=PATH=/home/pacs/.nvm/versions/node/v20.19.1/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin TimeoutStartSec=30 TimeoutStopSec=10 KillSignal=SIGINT [Install] WantedBy=multi-user.target ``` **Periksa konfigurasi dan sesuaikan:** - User dan Group : sesuai user server ini dijalankan. Biasanya 'pacs' dan 'pacs' - WorkingDirectory: letak codebase viewer. Default: /home/pacs/ohif-viewer/platform/app - ExecStart: pastikan direktori `ohif-viewer/platform/app/dist` dan file `ohif-viewer/platform/app/public/serve.json` ada di server - Environment: sesuaikan path dan versi node yang terinstall 2. Enable dan Start Service ```sh sudo systemctl daemon-reload sudo systemctl enable ohif-viewer sudo systemctl start ohif-viewer sudo systemctl status ohif-viewer ``` 3. Buka `ip.pacs.server:3000` di browser 4. Sebisa mungkin hindari penggunaan Firefox karena akan ada bug 'Random Order Juggling' di Study List. Namun viewernya tidak masalah ### Modifikasi OHIF Jika ada modifikasi yang dilakukan di kode OHIF, setelah merubah, ulangi dari langkah `Build OHIF` untuk mengimplementasikan perubahan ## 5. (Alt) Jika Ubuntu18 Gagal Build OHIF Jika Ubuntu 18 menyebabkan gagal build OHIF dan terlalu banyak perubahan yang beresiko. Maka gunakan OHIF Docker sebagai alternatif: 1. Buat dir dan buat config ```sh mkdir -p /home/pacs/ohif-docker nano /home/pacs/ohif-docker/default.js ``` 2. Copy dan sesuaikan config `default.js` ```js /** @type {AppTypes.Config} */ window.config = { routerBasename: '/', // whiteLabeling: {}, extensions: [], modes: [], customizationService: {}, showStudyList: true, // some windows systems have issues with more than 3 web workers maxNumberOfWebWorkers: 3, // below flag is for performance reasons, but it might not work for all servers showWarningMessageForCrossOrigin: true, showCPUFallbackMessage: true, showLoadingIndicator: true, experimentalStudyBrowserSort: false, strictZSpacingForVolumeViewport: true, groupEnabledModesFirst: true, maxNumRequests: { interaction: 100, thumbnail: 75, // Prefetch number is dependent on the http protocol. For http 2 or // above, the number of requests can be go a lot higher. prefetch: 25, }, expertise: false, //* Tambahan untuk enable expertise (CustomizableViewportOverlay) expertise_host: `http://152.42.173.210`, //* Tambahan untuk fetch data Expertise) pacs_document_host: `152.42.173.210`, pacs_document_port: 8080, // filterQueryParam: false, // defaultDataSourceName: 'dicomweb', defaultDataSourceName: 'local-proxy', dataSources: [ { namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', sourceName: 'local-proxy', configuration: { friendlyName: 'Static WADO Local Data', name: 'DCM4CHEE', qidoRoot: `http://152.42.173.210:5000/rs`, wadoRoot: `http://152.42.173.210:5000/rs`, qidoSupportsIncludeField: false, supportsReject: true, supportsStow: true, imageRendering: 'wadors', thumbnailRendering: 'wadors', enableStudyLazyLoad: true, supportsFuzzyMatching: false, supportsWildcard: true, staticWado: true, singlepart: 'video', bulkDataURI: { enabled: true, relativeResolution: 'studies', }, }, }, { namespace: '@ohif/extension-default.dataSourcesModule.dicomwebproxy', sourceName: 'dicomwebproxy', configuration: { friendlyName: 'dicomweb delegating proxy', name: 'dicomwebproxy', }, }, { namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', sourceName: 'dicomjson', configuration: { friendlyName: 'dicom json', name: 'json', }, }, { namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', sourceName: 'dicomlocal', configuration: { friendlyName: 'dicom local', }, }, ], httpErrorHandler: error => { // This is 429 when rejected from the public idc sandbox too often. console.warn(error.status); // Could use services manager here to bring up a dialog/modal if needed. console.warn('test, navigate to https://ohif.org/'); }, hotkeys: [ { commandName: 'incrementActiveViewport', label: 'Next Viewport', keys: ['right'], }, { commandName: 'decrementActiveViewport', label: 'Previous Viewport', keys: ['left'], }, { commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] }, { commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] }, { commandName: 'invertViewport', label: 'Invert', keys: ['i'] }, { commandName: 'flipViewportHorizontal', label: 'Flip Horizontally', keys: ['h'], }, { commandName: 'flipViewportVertical', label: 'Flip Vertically', keys: ['v'], }, { commandName: 'scaleUpViewport', label: 'Zoom In', keys: ['+'] }, { commandName: 'scaleDownViewport', label: 'Zoom Out', keys: ['-'] }, { commandName: 'fitViewportToWindow', label: 'Zoom to Fit', keys: ['='] }, { commandName: 'resetViewport', label: 'Reset', keys: ['space'] }, { commandName: 'nextImage', label: 'Next Image', keys: ['down'] }, { commandName: 'previousImage', label: 'Previous Image', keys: ['up'] }, // { // commandName: 'previousViewportDisplaySet', // label: 'Previous Series', // keys: ['pagedown'], // }, // { // commandName: 'nextViewportDisplaySet', // label: 'Next Series', // keys: ['pageup'], // }, { commandName: 'setToolActive', commandOptions: { toolName: 'Zoom' }, label: 'Zoom', keys: ['z'], }, // ~ Window level presets { commandName: 'windowLevelPreset1', label: 'W/L Preset 1', keys: ['1'], }, { commandName: 'windowLevelPreset2', label: 'W/L Preset 2', keys: ['2'], }, { commandName: 'windowLevelPreset3', label: 'W/L Preset 3', keys: ['3'], }, { commandName: 'windowLevelPreset4', label: 'W/L Preset 4', keys: ['4'], }, { commandName: 'windowLevelPreset5', label: 'W/L Preset 5', keys: ['5'], }, { commandName: 'windowLevelPreset6', label: 'W/L Preset 6', keys: ['6'], }, { commandName: 'windowLevelPreset7', label: 'W/L Preset 7', keys: ['7'], }, { commandName: 'windowLevelPreset8', label: 'W/L Preset 8', keys: ['8'], }, { commandName: 'windowLevelPreset9', label: 'W/L Preset 9', keys: ['9'], }, ], tours: [ { id: 'basicViewerTour', route: '/viewer', steps: [ { id: 'scroll', title: 'Scrolling Through Images', text: 'You can scroll through the images using the mouse wheel or scrollbar.', attachTo: { element: '.viewport-element', on: 'top', }, advanceOn: { selector: '.cornerstone-viewport-element', event: 'CORNERSTONE_TOOLS_MOUSE_WHEEL', }, beforeShowPromise: () => waitForElement('.viewport-element'), }, { id: 'zoom', title: 'Zooming In and Out', text: 'You can zoom the images using the right click.', attachTo: { element: '.viewport-element', on: 'left', }, advanceOn: { selector: '.cornerstone-viewport-element', event: 'CORNERSTONE_TOOLS_MOUSE_UP', }, beforeShowPromise: () => waitForElement('.viewport-element'), }, { id: 'pan', title: 'Panning the Image', text: 'You can pan the images using the middle click.', attachTo: { element: '.viewport-element', on: 'top', }, advanceOn: { selector: '.cornerstone-viewport-element', event: 'CORNERSTONE_TOOLS_MOUSE_UP', }, beforeShowPromise: () => waitForElement('.viewport-element'), }, { id: 'windowing', title: 'Adjusting Window Level', text: 'You can modify the window level using the left click.', attachTo: { element: '.viewport-element', on: 'left', }, advanceOn: { selector: '.cornerstone-viewport-element', event: 'CORNERSTONE_TOOLS_MOUSE_UP', }, beforeShowPromise: () => waitForElement('.viewport-element'), }, { id: 'length', title: 'Using the Measurement Tools', text: 'You can measure the length of a region using the Length tool.', attachTo: { element: '[data-cy="MeasurementTools-split-button-primary"]', on: 'bottom', }, advanceOn: { selector: '[data-cy="MeasurementTools-split-button-primary"]', event: 'click', }, beforeShowPromise: () => waitForElement('[data-cy="MeasurementTools-split-button-primary]'), }, { id: 'drawAnnotation', title: 'Drawing Length Annotations', text: 'Use the length tool on the viewport to measure the length of a region.', attachTo: { element: '.viewport-element', on: 'right', }, advanceOn: { selector: 'body', event: 'event::measurement_added', }, beforeShowPromise: () => waitForElement('.viewport-element'), }, { id: 'trackMeasurement', title: 'Tracking Measurements in the Panel', text: 'Click yes to track the measurements in the measurement panel.', attachTo: { element: '[data-cy="prompt-begin-tracking-yes-btn"]', on: 'bottom', }, advanceOn: { selector: '[data-cy="prompt-begin-tracking-yes-btn"]', event: 'click', }, beforeShowPromise: () => waitForElement('[data-cy="prompt-begin-tracking-yes-btn"]'), }, { id: 'openMeasurementPanel', title: 'Opening the Measurements Panel', text: 'Click the measurements button to open the measurements panel.', attachTo: { element: '#trackedMeasurements-btn', on: 'left-start', }, advanceOn: { selector: '#trackedMeasurements-btn', event: 'click', }, beforeShowPromise: () => waitForElement('#trackedMeasurements-btn'), }, { id: 'scrollAwayFromMeasurement', title: 'Scrolling Away from a Measurement', text: 'Scroll the images using the mouse wheel away from the measurement.', attachTo: { element: '.viewport-element', on: 'left', }, advanceOn: { selector: '.cornerstone-viewport-element', event: 'CORNERSTONE_TOOLS_MOUSE_WHEEL', }, beforeShowPromise: () => waitForElement('.viewport-element'), }, { id: 'jumpToMeasurement', title: 'Jumping to Measurements in the Panel', text: 'Click the measurement in the measurement panel to jump to it.', attachTo: { element: '[data-cy="data-row"]', on: 'left-start', }, advanceOn: { selector: '[data-cy="data-row"]', event: 'click', }, beforeShowPromise: () => waitForElement('[data-cy="data-row"]'), }, { id: 'changeLayout', title: 'Changing Layout', text: 'You can change the layout of the viewer using the layout button.', attachTo: { element: '[data-cy="Layout"]', on: 'bottom', }, advanceOn: { selector: '[data-cy="Layout"]', event: 'click', }, beforeShowPromise: () => waitForElement('[data-cy="Layout"]'), }, { id: 'selectLayout', title: 'Selecting the MPR Layout', text: 'Select the MPR layout to view the images in MPR mode.', attachTo: { element: '[data-cy="MPR"]', on: 'left-start', }, advanceOn: { selector: '[data-cy="MPR"]', event: 'click', }, beforeShowPromise: () => waitForElement('[data-cy="MPR"]'), }, ], tourOptions: { useModalOverlay: true, defaultStepOptions: { buttons: [ { text: 'Skip all', action() { this.complete(); }, secondary: true, }, ], }, }, }, ], }; function waitForElement(selector, maxAttempts = 20, interval = 25) { return new Promise(resolve => { let attempts = 0; const checkForElement = setInterval(() => { const element = document.querySelector(selector); if (element || attempts >= maxAttempts) { clearInterval(checkForElement); resolve(); } attempts++; }, interval); }); } ``` 3. Pull images ```sh docker login devone.aplikasi.web.id/gitea -u one docker pull devone.aplikasi.web.id/one/ohif-391:v2.0 ``` 4. Run ```sh docker run -d -p 3000:80/tcp -v /home/pacs/ohif-docker/default.js:/usr/share/nginx/html/app-config.js --name ohif-viewer-container devone.aplikasi.web.id/one/ohif-391:v2.0 ``` **Re run ohif-container** ``` docker pull devone.aplikasi.web.id/one/ohif-391:{versi_image_baru misal v3.0} docker stop ohif-viewer-container docker rm ohif-viewer-container docker run -d -p 3000:80/tcp -v /home/pacs/ohif-docker/default.js:/usr/share/nginx/html/app-config.js --name ohif-viewer-container devone.aplikasi.web.id/one/ohif-391:{versi_image_baru misal v3.0} ```
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mario/ohif-viewer#11