内視鏡画像システムに自作の閲覧システムを組み込む

内視鏡画像システムがもうすぐ UNITEA というシステムに変更になります。

この UNITEA は何らかの pacs に向けて dicom ファイルを出力するのがデフォルトのようで、pacs はフリーの pacs として最もメジャーな dcm4chee を使おうと思います。

dcm4chee は arc-light が主流になりつつあるようですが、設定が難しく私はまだうまく行きません。

以前のバージョンの dcm4chee であれば、ほぼ完全に自動設定できるシェルスクリプトを作っているので、誰でも簡単に設定でき、dicom ファイル閲覧には何の問題もありません。

ただし、「目」のアイコンが小さいのが難点です。
それと、検査・病理など他のデータベースとの統合が困難です。

dcm4chee でも見ることができ、もっと簡単なシステムでも見ることができるようにはできないか?
これが、何年も前からの大きなテーマでした。

イメージ

イメージはこんな感じです。

server は linux mint 19.3、client は linux mint 19.1 とします。

  • UNITEA から dcm4chee に dicom ファイルが出力され処理されます。
  • client の linux mint 19.1 から dcm4chee にアクセスすればそのまま dicom ファイルを閲覧することができます。
  • 自作の閲覧システムは Flask にある mypacs という pacs もどきのシステムで、dicom ファイル・weasis.jnlp を server から client に ftp でダウンロードして閲覧します。

必要な条件

flask で自作のプログラムを動かすのは簡単でしたが、問題は dicom ファイルと weasis.jnlp ファイルを linux mint 19.1 へダウンロードすることでした。

なぜなら flask が動いているのは linux mint 19.3、つまりサーバー上のプログラムなので、クライアントつまり linux mint 19.1 へファイルをコピーして、クライアント上のプログラムを起動する必要があるからです。

これを実現するためには、クライアントに

  • open ssh サーバーをインストールして scp でファイルコピー
  • ftp サーバーをインストールして ftp でファイルコピー

の2つの方法しか私には考えつきませんでした。

最終的に ftp を使用したのはその速度でした。
ftp の方が scp よりも数倍速いと思います。lan ケーブルが貧弱な環境でこれは大きなことです。
イントラネットなのでセキュリティよりも速度の方が重要です。

クライアントにシェルスクリプトを送ってそれを実行するので ssh も必要ですが、ファイルの転送は ftp を使います。

ファイルをクライアントに「ダウンロード」と書きましたが、実際にはクライアントに ftp サーバーを設定するので ftp サーバーへの「アップロード」です。

flask のファイル構造

サーバー側の flask のファイル構造は以下のようになっています。


mypacs
├── dcm.py
├── ext_py
│   └── dbshow.py
├── flask.sh
├── static
│   ├── css
│   └── js
├── templates
│   └── myapp
│       ├── alldata.html
│       └── home.html
├── weasis.jnlp
└── weasisTemplate
    ├── rmtmp.sh
    └── weasis.jnlp

dcm.py — flask の起動

dcm.py

from flask import Flask, render_template, redirect, request, Response, make_response
from ext_py.dbshow import DB_show

app = Flask(__name__)

@app.route('/')
def menu():
    return render_template('myapp/home.html', title='FLASK Menu') 
    
@app.route('/alldata')   
def alldata():
    dbs = DB_show()
    dbinfos = dbs.selectall()
    return render_template('myapp/alldata.html', title='alldata', alldata=dbinfos) 

if __name__ == "__main__": 
	app.run(debug=False, host='0.0.0.0', port=5000)	

コマンドラインから以下のようにして起動します。


python dcm.py

これをシェルスクリプトにしたのが「flask.sh」です。

pacsdb にアクセスして必要な情報を取得

dicom ファイルが dcm4chee にインポートされる際にさまざまな情報が pacsdb というデータベースに書き込まれます。それを読み込みます。
全検査をリストアップする方法は、


    def selectall( self ):
        conn = pymysql.connect(host=self.host,
            user=self.user,
            db=self.db,
            password=self.password,
            cursorclass=pymysql.cursors.DictCursor)
        try:
            with conn.cursor() as cursor:
                sql = "SELECT study.pk, study_datetime, patient.pat_id, pat_name, pat_birthdate, pat_sex, mods_in_study, study_desc FROM study INNER JOIN patient ON study.patient_fk = patient.pk;"
                cursor.execute( sql )
                return cursor.fetchall()
            conn.commit()
        finally:
            conn.close()

そして、抽出されたデータをテンプレートに貼り付けます。

クライアントにファイルを送る

pacs の原理は私にはわかりませんが、いずれにしても閲覧したい dicom ファイルと weasis.jnlp ファイルをクライアントに送らなければなりません。(ビューワが weasis の場合)

アクセスしてきたクライアントの IP アドレス

クライアントにファイルを送るためにはクライアントの情報が必要です。

ファイルを送る相手先の住所は、


from flask import request

@app.route('/selectfile/<string:pk>')
def selectfile( pk ):
    remoteIP = request.remote_addr

これで、remoteIP には「192.168.56.20」というような文字列が代入されます。

クライアントに向けて dicom ファイルを転送

アクセスしてきたクライアントに向けて dicom ファイルを転送します。
ここでは、「/tmp/dcmtmp」に送ります。


    def ftp_up( self, remoteIP, dcmarr ):
        ftp = FTP(
            remoteIP,
            "user",
            passwd="pass"
          )
        dt_now = datetime.datetime.now()
        datetimeinfo = dt_now.strftime('%Y%m%d%H%M%S/')
        self.thisdir = '/tmp/dcmtmp/' + datetimeinfo
        ftp.mkd( self.thisdir )
        for edcm in dcmarr:            
            serverfile = '/var/www/html/DICOM/' + edcm['filepath']
            localfile = self.thisdir + os.path.basename(serverfile)
            with open(serverfile, "rb") as f:
                ftp.storbinary("STOR " + localfile, f)  

/tmp/dcmtmp の中に、このメソッドにアクセスした瞬間の年月日時分秒(例えば「20220724173105」)というディレクトリを作成し、そこへ向けて dicom ファイルを ftp アップロードします。

これができるためには、クライアントに ftp サーバーがインストールされていて、そのユーザー名とパスワードがわかっていなければなりません。

weasis.jnlp ファイルを編集する

クライアントに向けて weasis.jnlp ファイルを転送するのですが、その weasis.jnlp ファイルがどのディレクトリを読むのかを編集する必要があります。

以下のような ./weasisTemplate/weasis.jnlp があるとして、


<?xml version="1.0" encoding="UTF-8"?>
  <jnlp spec="1.6+" codebase="http://localhost:8080/weasis" href="">
  <information>
    <title>Weasis</title>
    <vendor>Weasis Team</vendor>
    <description>DICOM images viewer</description>
    <description kind="short">An application to visualize and analyze DICOM images.</description>
    <description kind="one-line">DICOM images viewer</description>
    <description kind="tooltip">Weasis</description>
  </information>
  <security>
    <all-permissions />
  </security>
  <resources>
    <!-- Requires Java SE 6 update 10 release for jnlp extension without codebase (substance.jnlp) -->
    <j2se version="1.6.0_10+" initial-heap-size="128m" max-heap-size="512m" />
    <jar href="http://localhost:8080/weasis/weasis-launcher.jar" main="true" />
    <jar href="http://localhost:8080/weasis/felix.jar" />
    <extension href="http://localhost:8080/weasis/substance.jnlp" />
    <!-- Allows to get files in pack200 compression, only since Weasis 1.1.2 -->
    <property name="jnlp.packEnabled" value="true" />
    <!-- ================================================================================================================= -->
    <property name="jnlp.weasis.felix.config.properties" value="http://localhost:8080/weasis/conf/config.properties" />
    <property name="jnlp.weasis.felix.extended.config.properties" value="http://localhost:8080/weasis-ext/conf/ext-config.properties" />
    <property name="jnlp.weasis.weasis.codebase.url" value="http://localhost:8080/weasis" />
    <property name="jnlp.weasis.weasis.codebase.ext.url" value="http://localhost:8080/weasis-ext" />
    <property name="jnlp.weasis.gosh.args" value="-sc telnetd -p 17179 start" />
    <property name="jnlp.weasis.apple.laf.useScreenMenuBar" value="true" />
    <property name="jnlp.weasis.weasis.i18n" value="http://localhost:8080/weasis-i18n" />
    <!-- ================================================================================================================= -->
  </resources> 
  <application-desc main-class="org.weasis.launcher.WebstartLauncher">
    <argument>$dicom:get -l tmpdirectory</argument>
  </application-desc>
  </jnlp>

下の方の「tmpdirectory」が dicom ファイルが格納されている読み込むべきディレクトリですが、サーバーから転送したのは上の例では「/tmp/dcmtmp/20220724173105」に編集します。


    def editweasis( self ):
        with open( './weasisTemplate/weasis.jnlp', encoding="utf8" ) as f:
            data_lines = f.read()
        data_lines = data_lines.replace( "tmpdirectory", self.thisdir )
        with open( './weasis.jnlp', mode="w", encoding="utf8" ) as f:
            f.write(data_lines)

weasis.jnlp を添付する

アクセスしてきたクライアントに weasis.jnlp ファイルを添付して送ります。


@app.route('/selectfile/<string:pk>')
def selectfile( pk ):
    remoteIP = request.remote_addr
    dbs = DB_show()
    dcmarr = dbs.selectDfile( pk )
    dbs.ftp_up( remoteIP, dcmarr )
    
    response = make_response()
    response.data  = open('./weasis.jnlp', "rb").read()
    response.headers['Content-Type'] = 'application/octet-stream'
    response.headers['Content-Disposition'] = 'attachment; filename=./weasis.jnlp'
    return response

転送する dicom ファイルを一部削除するためのシェルスクリプト

このままでは、クライアントの「/tmp/dcmtmp」がいっぱいになってしまうので、古い方から削除して一定数に保つ必要があります。
以下のようなシェルスクリプトを作成します。

rmtmp.sh

#!/bin/bash

cd /tmp/dcmtmp
fc=`ls -l | grep ^d | wc -l`

if [ $fc -gt 5 ]; then
    echo $fc
    rm -r -d `ls -t . | tail -n+5`
fi

これも送って、


        with open("/home/user/pypacs/weasisTemplate/rmtmp.sh", "rb") as f:
            ftp.storbinary("STOR " + "/tmp/dcmtmp/rmtmp.sh", f) 

実行します。


    def rmtmp(self, remoteIP):
        host = remoteIP
        port = 22
        user = "heno"
        pswd = "moheno"
        client = SSHClient()
        client.set_missing_host_key_policy(AutoAddPolicy())
        client.connect(host, port=port, username=user, password=pswd)
        client.exec_command("mkdir /tmp/dcmtmp")
        client.exec_command("chmod 777 /tmp/dcmtmp/rmtmp.sh")
        client.exec_command("/tmp/dcmtmp/rmtmp.sh")
        client.close()

クライアントに ftp サーバーをインストール

ここではオフラインでインストールします。
インストール用のシェルスクリプトに以下の部分を加えます。
コピーするファイルは予め編集しておきます。


# vsftpd
cd ~/dcm_client/vsftpd
sudo dpkg -i vsftpd_3.0.3-9build1_amd64.deb
# file copy
sudo cp ~/dcm_client/other_file/vsftpd.chroot_list /etc/vsftpd.chroot_list
sudo cp ~/dcm_client/other_file/vsftpd.conf /etc/vsftpd.conf
#
sudo systemctl enable vsftpd
sudo systemctl restart vsftpd

サーバー側のプログラム

dcm.py

from flask import Flask, render_template, redirect, request, Response, make_response
from ext_py.dbshow import DB_show
app = Flask(__name__)
@app.route('/')
def menu():
    return render_template('myapp/home.html', title='FLASK Menu') 
    
@app.route('/alldata')   
def alldata():
    dbs = DB_show()
    dbinfos = dbs.selectall()
    return render_template('myapp/alldata.html', title='alldata', alldata=dbinfos) 
@app.route('/selectfile/')
def selectfile( pk ):
    remoteIP = request.remote_addr
    dbs = DB_show()
    dcmarr = dbs.selectDfile( pk )
    dbs.ftp_up( remoteIP, dcmarr )
    
    response = make_response()
    response.data  = open('./weasis.jnlp', "rb").read()
    response.headers['Content-Type'] = 'application/octet-stream'
    response.headers['Content-Disposition'] = 'attachment; filename=./weasis.jnlp'
    return response
 
if __name__ == "__main__": 
	app.run(debug=False, host='0.0.0.0', port=5000)	
dbshow.py

import pymysql
import os,shutil
import paramiko
import subprocess
import paramiko
import scp
import os
import datetime
from ftplib import FTP
from paramiko import SSHClient, AutoAddPolicy
from scp import SCPClient
class DB_show():
 
    def __init__( self ):           
        self.host = 'localhost'
        self.user = 'root'
        self.password = 'pass'
        self.db = 'pacsdb'
        self.thisdir = ''
                 
    def selectall( self ):
        conn = pymysql.connect(host=self.host,
            user=self.user,
            db=self.db,
            password=self.password,
            cursorclass=pymysql.cursors.DictCursor)
        try:
            with conn.cursor() as cursor:
                sql = "SELECT study.pk, study_datetime, patient.pat_id, pat_name, pat_birthdate, pat_sex, mods_in_study, study_desc FROM study INNER JOIN patient ON study.patient_fk = patient.pk;"
                cursor.execute( sql )
                return cursor.fetchall()
            conn.commit()
        finally:
            conn.close()
            
    def selectDfile( self, pk ):
        conn = pymysql.connect(host=self.host,
            user=self.user,
            db=self.db,
            password=self.password,
            cursorclass=pymysql.cursors.DictCursor)
        try:
            with conn.cursor() as cursor:
                sql = "SELECT filepath FROM files WHERE instance_fk IN (SELECT pk FROM instance WHERE series_fk IN (SELECT pk FROM series WHERE study_fk = %s));"
                cursor.execute( sql, pk )
                return cursor.fetchall()
            conn.commit()
        finally:
            conn.close()
    def editweasis( self ):
        with open( './weasisTemplate/weasis.jnlp', encoding="utf8" ) as f:
            data_lines = f.read()
        data_lines = data_lines.replace( "tmpdirectory", self.thisdir )
        with open( './weasis.jnlp', mode="w", encoding="utf8" ) as f:
            f.write(data_lines)
            
    def ftp_up( self, remoteIP, dcmarr ):
        self.rmtmp( remoteIP )
        ftp = FTP(
            remoteIP,
            "niwa",
            passwd="fuji"
          )
        dt_now = datetime.datetime.now()
        datetimeinfo = dt_now.strftime('%Y%m%d%H%M%S/')
        self.thisdir = '/tmp/dcmtmp/' + datetimeinfo
        ftp.mkd( '/tmp/dcmtmp/' + datetimeinfo )
        for edcm in dcmarr:            
            serverfile = '/var/www/html/DICOM/' + edcm['filepath']
            localfile = self.thisdir + os.path.basename(serverfile)
            print(localfile)
            with open(serverfile, "rb") as f:
                ftp.storbinary("STOR " + localfile, f)  
        # /tmp/dcmtmp clear
        with open("/home/user/pypacs/weasisTemplate/rmtmp.sh", "rb") as f:
            ftp.storbinary("STOR " + "/tmp/dcmtmp/rmtmp.sh", f) 
        # edit weasis.jnlp
        self.editweasis()
    def rmtmp(self, remoteIP):
        host = remoteIP
        port = 22
        user = "user"
        pswd = "pass"
        client = SSHClient()
        client.set_missing_host_key_policy(AutoAddPolicy())
        client.connect(host, port=port, username=user, password=pswd)
        client.exec_command("chmod 777 /tmp/dcmtmp/rmtmp.sh")
        client.exec_command("/tmp/dcmtmp/rmtmp.sh")
        client.close()
if __name__ == "__main__":   
    dbs = DB_show()
    infos = dbs.selectDB()
    imgs = dbs.selectID(36333)

クライアントの自動設定シェルスクリプト

setup.sh

#!/bin/bash

# open ssh
cd ~/dcm_client/ssh
sudo dpkg -i ncurses-term_6.1-1ubuntu1.18.04_all.deb
sudo dpkg -i openssh-sftp-server_1%3a7.6p1-4ubuntu0.7_amd64.deb
sudo dpkg -i ssh-import-id_5.7-0ubuntu1.1_all.deb
sudo dpkg -i openssh-server_1%3a7.6p1-4ubuntu0.7_amd64.deb

# vsftpd
cd ~/dcm_client/vsftpd
sudo dpkg -i vsftpd_3.0.3-9build1_amd64.deb

# tomcat
cd ~/dcm_client/tomcat
sudo dpkg -i libapr1_1.6.3-2_amd64.deb
sudo dpkg -i libeclipse-jdt-core-java_3.15.0+eclipse4.9-1~18.04_all.deb
sudo dpkg -i libtomcat9-java_9.0.16-3ubuntu0.18.04.2_all.deb
sudo dpkg -i tomcat9-common_9.0.16-3ubuntu0.18.04.2_all.deb
sudo dpkg -i libtcnative-1_1.2.21-1~18.04.1build1_amd64.deb
sudo dpkg -i tomcat9_9.0.16-3ubuntu0.18.04.2_all.deb

# java7
sudo mkdir /usr/local/java
sudo cp -r ~/dcm_client/jdk1.7.0_75 /usr/local/java

#weasis
sudo cp -r ~/dcm_client/weasis /var/lib/tomcat9/webapps
sudo chmod -R 0777 /var/lib/tomcat9

# file copy
sudo cp ~/dcm_client/other_file/vsftpd.chroot_list /etc/vsftpd.chroot_list
sudo cp ~/dcm_client/other_file/vsftpd.conf /etc/vsftpd.conf

#
sudo systemctl enable vsftpd
sudo systemctl restart vsftpd
mkdir /tmp/dcmtmp

インストールはあっという間です。

その後は、例によって java control パネルを起動して編集します。