Table of Contents |
guest 2025-04-29 |
For containers to talk to each other, a common network is required. It is a two step process:
Network is started using ~/bin/startNetwork.sh
and equivalent stop script by rproxy
user. Network name is docker_network
.
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.
A http splitter to sub-resources. So far, a generic nginx page, but switched will be added. General info in table
User | rproxy |
Docker Image | nginx |
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 |
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.
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 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
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.
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
Based on turn-key solution
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.
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
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
. ~/virtualenv/p3.8/bin/activate
pip install invenio-cli
invenio-cli init rdm -c v6.0
docker-compose.full.yml
networks:
default:
external:
name: docker_network
frontend
to APP_ALLOWED_HOSTS
in invenio.cfg
frontend
to avoid collision with rproxy
.db
.
Always run build
and setup
after changes to invenio.cfg.
invenio-cli containers build
invenio-cli containers setup
invenio-cli containers start
invenio-cli containers stop
rproxy
exits if the proxied service is not available, so start rproxy
after services.
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.
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.
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
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:
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.
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
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>
);