Table of Contents

guest
2025-04-29
Klimt installation
   Klimt reverse proxy
   Data Repository
   XNAT installation
     XNAT OHIF notest

Klimt installation


Installation of Klimt, the resource hub

Reverse proxy



Klimt reverse proxy


Reverse proxy docker image

Common network

For containers to talk to each other, a common network is required. It is a two step process:

  • Manage common network
  • Connect containers to the common network

Common network management

Network is started using ~/bin/startNetwork.sh and equivalent stop script by rproxy user. Network name is docker_network.

Connect containers

Add

network:
   external:
      default:
        name: docker_network

to your docker-compose.yaml. This is assuming, your docker-compose.yaml doesn't do anything special with the network already.

Docker image

A http splitter to sub-resources. So far, a generic nginx page, but switched will be added. General info in table

Userrproxy
Docker Imagenginx
Docker compose~rproxy/config/rproxy-compose.yaml
Page X config~rproxy/config/conf.d/X.conf
Startup script~rproxy/bin/startRproxy.sh
Stop script~rproxy/bin/stopRproxy.sh
Configuration reolad:~rproxy/bin/reloadRproxy.sh

Default landing page, default.conf, klimt.fmf.uni-lj.si

The default landing page points to a rproxy managed site, at ~/www/landing. The SSL is terminated with a NIXLJU-CA certificate, and HTTPS is enforced:

#HTTP -> redirect
server {
    listen       80;
    listen  [::]:80;
    server_name klimt.fmf.uni-lj.si;
    return 301 https://$host$request_uri;
}

#HTTPS
server {
    listen 443 ssl;
    listen [::]:443 ssl; 
    server_name klimt.fmf.uni-lj.si;
    
    [...]
    
    #SSL
    ssl_certificate /var/www/klimtBundle.crt;
    ssl_certificate_key /var/www/klimt.key;
    
    [...]
    
    location /{
        root /var/www/landing;  
    }


The first reverse proxy at rp0.fmf.uni-lj.si is configured with NIXLJU-CA_chain.crt certificate, so connection to klimt is trusted.

Invenio, invenio.conf, invenio.fmf.uni-lj.si

Identically, HTTPS is strictly enforced as for the default page. server_name is set to invenio.fmf.uni-lj.si to enable server name based routing. The processing is delegated to Invenio reverse proxy frontend. A potential scheduler/load-balancer can be extended by adding equivalent sites.


upstream frontend {
  server frontend:443;
  #could add more servers for load balancing
}

#HTTPS
server {
    listen 443 ssl;
    listen [::]:443 ssl; 
    server_name invenio.fmf.uni-lj.si;

    [...]

    location /{
        proxy_ssl_trusted_certificate /var/www/NIXLJU-CA_chain.crt;
        proxy_pass https://frontend;
        proxy_redirect https://frontend https://invenio.fmf.uni-lj.si;
    }


proxy_ssl_trusted_certificate directive makes the NIXLJU-CA certificates trusted by the frontend proxy.

SSL on nginx

SSL helps protect the data between servers.

Certificates must be bundled together, ie. nginx has no SSLCACertificateFile variable in setup. The order matters, the server certificate should precede the CA certificate (chain). For NIX certificates,

cat frontend.crt NIXLJU-CA_chain.crt > frontendBundle.crt

Reverse proxy at FMF

Setup after klimt reboot

If klimt is rebooted, its IP address might change. For named services, this is OK, but at the reverse proxy of the faculty, a temporary name is assigned to maintain name resolution at klimt. The temporary name is stored in /etc/hosts on rp where fixed IP address must be used. So, at rp edit /etc/hosts/ with the correct/new IP address.

Resolved cache

IPs get stuck in cache of systemd-resolved. Clear cache by running:

systemd-resolve --flush-caches

Apache also keeps track of old IPs, so it has to be restarted to drop it:

/etc/init.d/apache2 restart 




Data Repository


Data Repository

Based on turn-key solution

Installation

Had troubles with setting-up. Apparently, native python version should match image python version. Since no python3.9 image is available at docker, and debian 11 comes only with python3.9, of the two options downgrading of python was chosen.

Python downgrade

Follow instructions. Essentially:

sudo apt update && sudo apt upgrade
wget https://www.python.org/ftp/python/3.8.12/Python-3.8.12.tar.xz
sudo apt install build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev 
     libssl-dev libsqlite3-dev libreadline-dev libffi-dev curl libbz2-dev -y
mkdir software/packages
mv Python-3.8.12.tar.xz software/packages/
mkdir software/build && cd software/build
tar xvf ../packages/Python-3.8.12.tar.xz
./configure --prefix=~/software/install/python3.8 --enable-optimizations --enable-shared
make -j 6 && make install

Setup virtualenv for invenio

sudo su invenio
mkdir -p virtualenv/p3.8
export LD_LIBRARY_PATH=~X/software/install/python3.8/lib:$LD_LIBRARY_PATH
~X/software/install/python3.8/bin/python3.8 -m venv ~/virtualenv/p3.8

Follow Invenio instructions

. ~/virtualenv/p3.8/bin/activate
pip install invenio-cli
invenio-cli init rdm -c v6.0

Correct some recently updated packages that break Invenio build

  • Add RUN pip install MarkupSafe==2.0.1 to created Dockerfile
  • Add to docker-compose.full.yml
networks: 
   default: 
      external: 
         name: docker_network

  • Add frontend to APP_ALLOWED_HOSTS in invenio.cfg
  • Remove exposed ports 80 and 443 from frontend to avoid collision with rproxy.
  • TODO: Remove exposed port 5432 of db.

Always run build and setup after changes to invenio.cfg.

Build

invenio-cli containers build
invenio-cli containers setup

Run containers

invenio-cli containers start
invenio-cli containers stop

rproxy exits if the proxied service is not available, so start rproxy after services.

Reroute static

One solution




XNAT installation


XNAT installation

Create a user xnat and add him to the docker group. Do the following as xnat using its home as a core of the environment.

Follow dockerizet XNAT instructions. Checkout the configuration directory and copy default environment.

From the setup directory, link xnat-data and postgres-data out from the configuration and into the storage area - to split configuration in home from storage in data.

sudo mkdir /data/xnat-data
sudo mkdir /data/postgres-data
sudo su xnat
cd ~/xnat-docker-compose
ln -s /data/xnat-data
ln -s /data/postgres-data

Some modifications might be necessary in docker-compose.yaml. For example, for XNAT to report on port 8090, lines 56 and 57 of the original should be changed to:

56 ports:
57      "8090:80"

and the port number should be added to proxy_redirect in nginx/nginx.conf, line 52 and 53

52     proxy_redirect                      http://xnat-web:8080 $scheme://localhost:8090;
53      proxy_set_header Host               $http_host;

However, should you want to use the default port, no modification should be required.

Update XNAT configuration

  • When loging in as admin to XNAT, make sure you update the SMTP settings. In my case, I had to switch to port 587, and turn identification and SSL tunneling to ON.

  • In Administration->Site administration->DICOM SCP Receivers set AET name of the PACS to the desired value. In my case I used RUBENS as the AET name.

  • Still testing the best option to merge series. Right now, I've set Prevent Cross-modality Session Merger to disabled (the text says merging is allowed) under Miscallaneous.

  • Edit Project Routing. Normally XNAT looks at a set of predefined fields to match data and project. I basically want all data to go into a single project, and a cludge to do so is putting (0010,0020):^([0-9]+)/[0-9]+$:1 t:^(.*)$ r:IRAEMM into Site Administration -> Session Upload & Anonymization -> Project Routing (Site Wide) configuration window. This routes anything that matches N/M where N and M are (multi-digit) numbers to IRAEMM project.

XNAT-OHIF

Install yarn:

curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt update
sudo apt install yarn

Get OHIF XNAT and compile

git clone --recurse-submodules https://bitbucket.org/icrimaginginformatics/ohif-viewer-xnat-plugin.git
cd ohif-viewer-xnat-plugin
git submodule update --init --recursive
build_plugin.sh




XNAT OHIF notest


Notes on XNAT OHIF

  • Viewer components: Toolbar is set in extensions/xnat/src/toolbarModule.js. Here, the items on the toolbar are connected to the actual software implementations. Here I am exposing two tools, a 3D brush tool and a 3D HU gated brush tool which appear as Manual and Smart CT in the menu.

    const definitions = [
            [...]
            {
                    id: 'brushTools',
                    label: 'Mask',
                    icon: 'xnat-mask',
                    buttons: [
                    {
                            id: 'Brush',
                            label: 'Manual',
                            [...]
                            commandOptions: { 
        return true;
    
    

}

As far as I can tell, the algorithm is the following - as mouseDown is about to happen (some controversy there already), an initialization is performed (_startPainting). A watchdog is started that will issue an _endPainting (not explicitly shown) through _startListeningForMouseUp. A tool handler _paint is invoked for the current point. The processing of the first point completes.

Intermediate points get directed towards _paint through mouseDragEvent but not in mouseMoveEvent, toolName: PEPPERMINT_TOOL_NAMES.BRUSH_3D_TOOL },
},
{
id: 'Brush3DHUGatedTool',
label: 'Smart CT',
[...]
commandOptions: { toolName: PEPPERMINT_TOOL_NAMES.BRUSH_3D_HU_GATED_TOOL },
},
{
id: 'SphericalBrush',
label: 'Spherical',
commandOptions: { toolName: PEPPERMINT_TOOL_NAMES.XNAT_SPHERICAL_BRUSH_TOOL },
},

            [...]
        ]       
        [...]
        }
]
```

  • Smart CT: I am using Smart CT is a role model for a hypothetical Smart PET, where a predefined threshold (SUV) is set on PET and a brush is used to "paint" all lesions in the area.
    In toolbarModule.js the Smart CT is set up to be a PEPPERMINT_TOOL_NAMES.BRUSH_3D_HU_GATED_TOOL, with mappings to classes given in extensions/xnat/src/peppermint-tools/toolNames.js:

    const TOOL_NAMES = {
            [...]
            BRUSH_3D_TOOL: 'Brush3DTool',
            BRUSH_3D_HU_GATED_TOOL: 'Brush3DHUGatedTool',
            XNAT_SPHERICAL_BRUSH_TOOL: 'XNATSphericalBrushTool',
            [...]
    }
    
    
  • Brush3DHUGatedTool: extensions/xnat/src/peppermint-tools/tools/segmentation/Brush3DHUGatedTool.js, descends from Brush3DTool:

    export default class Brush3DHUGatedTool extends Brush3DTool {
    
    
  • Brush3DTool: Lineage: BaseTool->BaseBrushTool->BrushTool->Brush3DTool. Everything down to BrushTool is part of Cornerstone3D, one of 1848 external node dependencies of ohifxnatviewer. Cornerstone tools are part of the ohif-xnat-plugin git repository under node_modules/cornerstone-tools:

    import cornerstoneTools from 'cornerstone-tools';
    [...]
    const { BrushTool } = cornerstoneTools;
    [...]
    export default class Brush3DTool extends BrushTool {
    
    
  • brush has a radius, or possibly, depth, settable through configuration object

    const radius = configuration.radius;
    
    
  • brush works in 2D only, although 3D is in its name. TBD why.

  • Spherical, which is an XNATSphericalBrushTool, does 3D markup. Smart PET should therefore descend from it.

  • XNATSphericalBrushTool is a cornerstone-tools Sphericradius: 10, minRadius: 1, maxRadius: 50, fillAlpha: 0.2,alBrushTool

    import csTools from 'cornerstone-tools';
    [...]
    const { SphericalBrushTool } = csTools;
    [...]
    export default class XNATSphericalBrushTool extends SphericalBrushTool {
            constructor(props = {}) {
                    ...
            }
            preMouseDownCallback(evt) {
                    ...
                    super.preMouseDownCallback(evt);
            }
    }
    
    
  • all additional magic is happening in preMouseDownCallback.

  • basic behaviour from BaseBrushTool from cornerstone, lineage is BaseTool->BaseBrushTool->BrushTool->SphericalBrushTool.

  • in BaseTool the tool interaction model is exposed - the tool consists of declared only callbacks to standard interface events, such as:

    • BaseTool~preMouseDownCallback
    • BaseTool~postMouseDownCallback
  • in BaseBrushTool the callbacks get elaborated and explicit:

    preMouseDownCallback(evt) {
            const eventData = evt.detail;
            const { element, currentPoints } = eventData;
    
            this._startPainting(evt);
    
            this._lastImageCoords = currentPoints.image;
            this._drawing = true;
            this._startListeningForMouseUp(element);
            this._paint(evt);
    
            return true;
    }
    
    
  • As far as I can tell, the algorithm is the following - as mouseDown is about to happen (some controversy there already), an initialization is performed (_startPainting). A watchdog is started that will issue an _endPainting (not explicitly shown) through _startListeningForMouseUp. A tool handler _paint is invoked for the current point. The processing of the first point completes.

  • Intermediate points get directed towards _paint through mouseDragEvent but not in mouseMoveEvent, so there must be a difference between the two, which is not yet apparent.

  • In _endPainting the process is reversed - the last point is shipped to _paint and the configuration is dissolved.

  • _paint is abstract and conscientiously such - even an error is thrown - BaseBrushTool must be extended.

XNATSphericalPETThresholdBrush

I started a class XNATSphericalPETThresholdBrushTool. I based it off XNATSphericalBrushTool as it already does the 3D handling. To do the thresholding, I copied _paint from SphericalBrushTool and made sure the new class imported all relevant sources. Particularly:

import csCore from 'cornerstone-core';
        const { getCircle, drawBrushPixels } = csTools.importInternal('util/segmentationUtils');
        const segmentationModule = csTools.getModule('segmentation');


Then, the viewer must be told we added the new class, and that is spread over multiple classes:

  • ohifviewerxnat/extensions/xnat/src/peppermint-tools/index.js
  • ohifviewerxnat/extensions/xnat/src/peppermint-tools/tools/index.js
  • ohifviewerxnat/extensions/xnat/src/peppermint-tools/toolNames.js
  • ohifviewerxnat/extensions/xnat/src/toolbarModule.js
  • ohifviewerxnat/extensions/xnat/src/init.js
    In all cases I used XNATSphericalBrushTool as a guideline and added PET tool to the same list.

To set the threshold, we need first a place for the setting to live, Standard settings (brush size, etc.) are under segmentationModule, which is accessed through csTools. This is from Brush3DHUGatedTool:

const radius = segmentationModule.configuration.radius;

segmentationModule is part of cornerstoneTools, but luckily enough the module gets extended in ohifviewerxnat/extensions/xnat/src/init.js:

const config = Object.assign({}, getDefaultConfiguration(), configuration);
extendSegmentationModule(segmentationModule, config);

with extendSegmentationModule defined in ohifviewerxnat/extensions/xnat/src/peppermint-tools/modules/extendSegmentationModule.js. The function getDefaultConfiguration() in init.js already attaches some HU gating related variables to the native cornerstone configuration, and I see it as a place to attach
our petThreshold as well:

const getDefaultConfiguration = () => {
        ...
        const defaultConfig = {
                ...
                petThreshold: 3,
                ...
        }
  ...
  return defaultConfig;
};

In extendedSegmentationModule.js we then copy the threshold to the segmentationModule configuration:

configuration.petThreshold=config.petThreshold;

The UI for segmentation settings is ohifviewerxnat/extensions/xnat/src/components/XNATSegmentationSettings/XNATSegmentationSettings.js, where we generate another custom slider for PET threshold. The step and max should still be configured - on true PET images they should be in units of SUV, for test image (uncorrected) I set a broader range.

 const PETThreshold = (
    <div className="settings-group" style={{ marginBottom: 15 }}>
      <div className="custom-check">
        <label>PET Threshold</label>
      </div>
      <CustomRange
        label="PET Threshold"
        step={100}
        min={0}
        max={10000}
        value={configuration.petThreshold}
        onChange={event => save('petThreshold', parseInt(event.target.value))}
        showValue
      />
    </div>
  );

   return (<div className="dcmseg-segmentation-settings">
           <div className="settings-title">
           <h3>Mask ROI Settings</h3>
           <button className="return-button" onClick={onBack}>
                        Back
           </button>
           </div>
           {BrushSize}
           {SegmentFill}
           {SegmentOutline}
           {PETThreshold}
            </div>
        );