HOME > Information > QNAP Dockerにdcm4chee PACSを構築する方法|Docker ComposeコードをAIで生成 | 名古屋~春日井の脳神経外科 | 勝川脳神経クリニック
QNAP Dockerにdcm4chee PACSを構築する方法|Docker ComposeコードをAIで生成
このページは、読むためのものではありません
AIに読ませて、PACSの作り方を教えてもらうためのページです。
使い方
このページのURLを含む下記をコピーして、ChatGPTなどのURLを参照できるAIに、次のように質問してください。
🔴🔴🔴【次のURLの記事内容と記載順に忠実に、QNAPでdcm4chee PACSを構築する手順を初心者向けに詳しく説明してください。
https://kachigawanouge.com/pacs
「PACSの素-a」「PACSの素-b」の両方について、役割説明だけでなく、記事の仕様に沿った完成コードの生成方法、実行方法、OsiriX連携、ID連携、自動転送、全体構成まで省略せず示してください。ページ本文だけでなく、末尾の「PACS_NO_MOTO_A_BEGIN~END」「PACS_NO_MOTO_B_BEGIN~END」および「追記:OsiriX MD版」の参照コードまで必ず確認してください。】🔴🔴🔴
あとは、分からないことをAIに追加で質問してください。
QNAP上の保存先、使用するポート、AE Title、OsiriXのIPアドレスなどを伝えると、あなたの環境に合わせて、より具体的に説明してもらえます。
まずは、そのままAIに聞いてみてください。きっとうまくいくと思います。
※AIが生成したコードは、必ず内容を確認し、実際の患者情報を保存していない検証環境で動作を確認してください。
※このページだけは、クリニックのホームページとしては少し異質な構成です。まだ未熟な構成ではありますが、興味のある方にとって大切なコンテンツになるよう、少しずつカスタマイズしていきます。細かな変更を繰り返していくため、数か月前の内容と現在の内容で整合性が取れないこともあるかと思いますが、温かく見守ってください。
QNAPでdcm4chee PACSを立ち上げるYAMLの作り方
これまでの技術記事では、完成したシェルスクリプトやDocker Composeファイルをそのまま掲載し、読者がコピーして実行する形式が一般的でした。
しかし、実際のサーバー構築では、NASの保存先、使用するポート、AE Title、コンテナのバージョン、データの永続化先などが環境ごとに異なります。
完成コードだけをコピーしても、そのままでは動かず、利用者自身が修正しなければならないことが少なくありません。
完成コードそのものを配布するのではなく、方法を記載します。そのため、それぞれの環境に合ったコードを生成させることができます。
このページでは、QNAP QuTS heroとContainer Station上で、次の3つのコンテナを使用してPACSを構築するための「AI生成用プロンプト」の例を提示します。
下記は実機で動作確認した構築条件のプロンプトになります。PACSを立ち上げるコードを教えてくれると思います。後述の≪PACSの素-a≫の作成になります。
***の表記は任意となります。
【1. 次の公式WikiのDocker Compose最小構成を参考に、QNAP QuTS hero用の単一Bashスクリプトを作成してください。
https://github.com/dcm4che/dcm4chee-arc-light/wiki/Run-minimum-set-of-archive-services-on-a-single-host
2. 保存先は/share/a1/dcm4chee、HTTPは***、AE Titleは***、タイムゾーンはAsia/Tokyoとします。
3. イメージはslapd 2.6.10-34.2、postgres 17.4-34、arc-psql 5.34.2に固定し、サービスはldap、db、arcだけとします。
4.全データを保存先配下へbind mountし、mkdir -p、chmod -R 777、固定コンテナ名、unless-stopped、json-file 10m×5を設定してください。
5.Docker Compose v2を使用し、最後にdocker compose up -d、docker ps、docker logs -f dcm4chee-arcを実行してください。
6.回答は説明なしで、そのまま実行可能な完全なBashスクリプトだけを1つのコードブロックで出力してください。】
●上記まではPACS本体の作成となります、下記は、電カルからのID出力で「ビューワーを開く」と、「名前、生年月日をPACSで変更、登録する」を同時にする、
いわゆるビューワー連携とID連携を同時にする、コードのためのプロンプトになります。AIの性能にもよりますが、個人的な感覚ですが、上記は95%以上の完成度、下記は80%ほどの印象です、
生成後に少し手を加える必要性はありますが、努力すればできる範囲です。下記は、後述の≪PACSの素-b≫の作成になります。
【1. QNAP QuTS hero用に、docker-compose.ymlとwatch.ps1を生成して起動する、単一でそのまま実行可能な完全なBashスクリプトを作成してください。
2. Bashは#!/usr/bin/env bash、set -e、ao2=”/share/a1/pswatch”とし、”$ao2/id/sumi”をmkdir -pで作成し、生成後に”$ao2″全体をchmod -R 777にしてください。
3. Composeはpswatchサービスのみ、image=mcr.microsoft.com/powershell:latest、container_name=pswatch、restart=unless-stopped、TZ=Asia/Tokyoとしてください。
4. 環境変数はWATCH_ROOT=/app/id、BASE_ARC=http://dcm4chee-arc:8080/dcm4chee-arc、AETITLE=***、MAC_HOST=192.168.***.***、MAC_PORT=***、SERVER_NAME=***、FALLBACK_PATIENT_ID=***としてください。
5. “$ao2/id:/app/id:rw”と”$ao2:/app/script:rw”をマウントし、working_dir=/app/script、command=[“pwsh”,”-File”,”watch.ps1″]、外部ネットワークdcm4chee_defaultを使用してください。
6. watch.ps1は必要な環境変数を確認し、WATCH_ROOT直下の*.txtをFileSystemWatcherのCreatedで監視し、200ms待って処理し、最後は無限待機してください。
7. Shift-JISの1行「ID,漢字氏名,カナ氏名,性別,誕生日」を解析し、漢字氏名は使用せず、カナの空白を^にしてDICOM PNのAlphabeticへ格納し、性別1=M・2=F、誕生日は数字8桁のYYYYMMDDとしてください。
8. QIDO-RSでPatient IDを検索し、未登録またはカナ氏名・誕生日・性別に変更がある場合だけpatientsへDICOM JSONをPOSTし、PUTは使わず、既存患者更新時だけ既存00100021 Issuerを再利用してください。
9. 最新20件のStudyを検索してその中の最新MRを優先し、なければ最新の任意Study、Studyなしまたは検索失敗ならフォールバックIDを使用してください。
10. OsiriXへRetrieve(then=open、retrieveOnlyIfNeeded=yes)を送り、成否にかかわらず1秒後にDisplayStudyを送り、最後に必ず入力をid/sumiへ移動し、docker compose up -d、docker ps | grep pswatch、docker logs -f pswatchを実行し、回答は説明なしのコードブロック1つだけにしてください。】
QNAPでDCM4CHEE立ち上げ方法(実際の手順)
上記1~18の手順で、PACSの構築から各種連携設定まで完了します。
作業に慣れている場合は、手順6以降をおおむね20分程度で進めることができます。
1.QNAP購入
2.Qfinder Proで検出、PC側でIP合わせてブラウザにつなげる
3.QTS上でファームウェア更新
4.バックアップ/復元→NASの再初期化
5.QuTS heroのインストールを選択し実行
6.Container Stationをインストール
7.QNAPにフォルダを作成(フォルダ名:a1)
8.container-station-dataはa1配下に作成
9.QNAPへSSH接続ON:[コントロールパネル]-[ネットワークとファイルサービス]-[Telnet/SSH]→[SSH接続を許可する]
10.ssh *******@192.168.***.***でログイン
11.PACSの素-aをコピペ実行(DCM4CHEEの立ち上げ)
12.PACSの素-bをコピペ実行(ビューワー連携/ID連携の同時立ち上げ)
13.DCM4CHEEにOsiriX登録(Configuration→AElist→NewAET(name,IP,port,AETitle入力)
14 OsiriXにDCM4CHEE登録:場所→DCM4CHEEのIP,Port,AETitle入力
15.ビューワー連携のためOsiriX環境設定:リスナー→XML通信ON
16.DICOM取り込みでイシュアなしにする(下記)
Configuration→Devices→dcm4chee-arc→Child Objects→Network AEs→現在のAE→
Network AE Extension→Archive Network AE→Child Objects→
Attribute Coercion (New)→SupplementIssuerOfPatientID
(DICOM Transfer Role:SCP、Condition:なし、
DICOM Attribute Coercion Parameters:IssuerOfPatientID= )
17.PACS間のリアルタイム転送(自動転送)の設定(下記)
Configuration→AElist→NewAET(転送先のname,IP,port,AETitle入力)
Configuration→Devices→dcm4chee-arc→Extension→Device Extension→Archive
Device→Exporter Descriptor
(Exporter ID:*** ,Exporter URI:dicom:転送先のname,Queue Name:Export Tasks
(Export) ,Archive Application Entity (AE) title:現在ののname,
Exporter Property:failureHandling=RETRY とretryInterval=PT5M )
Configuration→Devices→dcm4chee-arc→Extension→Device Extension→
Archive Device→Export Rule
(Name:転送先のname,Export Entity:Study ,Exporter ID:***)
18.DICOM受信、ビューワー連携、ID連携、自動転送が作動しているか確認。
注意
前述のプロンプトは、PACS構築コードの作成を補助するためのものです。AIが生成したコードの完全性や安全性を保証するものではありません。
生成されたコードは、必ず内容を確認し、実データを保存していない検証環境で動作を確認してください。
本記事で扱う3コンテナ構成は、閉域ネットワーク上の技術検証を目的とした最小構成です。認証、アクセス制御、監査など、診療環境で必要となるすべての要件を満たす構成ではありません。
本記事は2025年12月10日に、dcm4chee-arc 5.34.2を使用して動作確認しています。最新版では設定やイメージの組み合わせが異なる場合があります。
筆者の検証では、PACS本体の生成コードは軽微な修正で動作しました。一方、OsiriX連携部分は、電子カルテの出力形式、文字コード、ネットワーク構成、OsiriXのバージョンによる差が認められ、生成後の確認と修正が必要でした。
本記事の検証環境では、各端末およびコンテナからの書き込みを容易にするため、chmod 777を使用しています。実運用では、QNAPの共有フォルダ権限、利用ユーザー、コンテナのUID・GIDを確認し、必要に応じてアクセス権限を適切に制限してください。
実際は、chmod 777は禁止し、必要最小限の権限を使用してください。
構築作業中だけSSHを有効にし、構築完了後、SSHを常用しない場合は無効化してください。
診療用途で使用する場合は、施設の医療情報安全管理責任者、システム担当者および保守事業者とリスク評価を行い、厚生労働省「医療情報システムの安全管理に関するガイドライン」などに基づいて、必要な安全管理措置と事業継続計画を整備してください。
最後に
本記事は、筆者が個人の趣味および技術検証の一環として作成しているものです。
内容の正確性には配慮していますが、将来的な改善点もあります、PACS作ってみたい方の参考になればと思います。
サブとしてですが検証環境では、DICOM受信、OsiriX連携、患者情報連携および別PACSへの転送が継続して動作しています。
私自身はQNAP.Dockerでのdcm4cheeは信頼していますが、これは特定のQNAP機種、ソフトウェアバージョンおよびネットワーク構成での結果であり、他の環境での動作、診療用途における安全性・可用性、法令・ガイドラインへの適合を保証するものではありません。
うまくいかない場合、趣味の範囲ではありますが、何かアドバイスできることもあるかもしれませんので、気楽にお問い合わせいただければ、返答します。
医療現場のIT化・システム内製に関心のある方へ
当院では、PACS、FileMaker、PowerShell、Python、Linuxなどを活用し、医療現場で使用するシステムの構築、連携、自動化、保守を院内で進めています。
今回ご紹介したdcm4chee PACSのような仕組みに興味があり、通常の医療業務に加えて、院内ITの改善にも主体的に関わりたい方を募集しています。
▶ 採用情報トップページ
✅ 診療放射線技師 求人ページ
✅ 看護師 求人ページ
✅ 臨床検査技師 求人ページ
✅ 医療事務 募集ページ
現時点で高度なIT技術を持っている必要はありません。新しい技術を学ぶことが好きで、仕組みを理解しながら医療現場をより良くしたい方を歓迎します。院内IT業務への関与内容に応じて、別途手当を設けています。
※本記事は医療法人 青漣会 勝川脳神経クリニック 理事長 青山 国広 医師(日本脳神経外科専門医/日本脳卒中専門医/頭痛指導医)が監修しています。
監修日:2025年12月24日
▶監修医師のプロフィールはこちら
以下に、「PACSの素-a」と「PACSの素-b」の参考コードを掲載します。
「PACSの素-a」はdcm4chee本体を構築するコード、「PACSの素-b」は患者ID連携、およびdcm4cheeからOsiriXへの画像取得・表示を行うコードです。
これらのコードは、このページのURLをAIに提示した際に、AIがシステムの構成や動作を参照し、より具体的な構築手順を説明できるよう、続きを読む以降に掲載しています。
なお、掲載しているコードは当院の環境で動作確認したものですが、2025年12月時点のプロトタイプです。環境やソフトウェアのバージョンによっては、そのままでは動作しない可能性があります。診療環境へ直接導入せず、必ず個人情報を含まない検証環境で内容を確認し、必要な修正を行ったうえで使用してください。
PACSの素 参考コード
以下に、「PACSの素-a」と「PACSの素-b」の参考コードを掲載します。
「PACSの素-a」はdcm4chee本体を構築するコードです。
「PACSの素-b」は患者ID連携、およびdcm4cheeからOsiriXへの画像取得・表示を行うコードです。
PACS_NO_MOTO_A_BEGIN
PACSの素-a
用途:dcm4chee本体の構築
コード開始
#!/usr/bin/env bash
#############################################################################
# dcm4chee(ldap + postgres + arc)を Docker で起動する bash スクリプト
# - QNAP QuTS hero / ZFS 系専用
# - 共有フォルダ a1 配下に dcm4chee を作成
# - docker compose のみ使用
#
# 【公開用参考コード】
# - コンテナ内部の標準ポートは記載しています。
# - ホスト側の待受IP・公開ポート・AE Title・DBパスワードは
# 環境変数から受け取り、実環境固有値をコード内に掲載しません。
# - LDAP、PostgreSQL、管理用ポートはホスト側へ公開しません。
#############################################################################
set -euo pipefail
#############################################################################
# 0) QNAP QuTS hero / ZFS 系 固定設定
#############################################################################
# QNAP の共有フォルダ a1
# 実体は例:/share/ZFS18_DATA/a1 だが、スクリプトでは /share/a1 を使う
ao1="/share/a1/dcm4chee"
StorageHost="${ao1}/storage"
#############################################################################
# 0-1) 実環境固有値は環境変数で指定
#############################################################################
# 安全側の既定値:127.0.0.1 はNAS本体からのみ接続可能。
# 院内LANから接続する場合だけ、PACS_BIND_ADDRESS にNASの院内LAN側IPを指定する。
# 0.0.0.0 の指定や、ルーターでのポート転送は避ける。
PACS_BIND_ADDRESS="${PACS_BIND_ADDRESS:-127.0.0.1}"
# 公開用コードには実際の値を記載しない。
# 実行前に利用環境側で必ず指定する。
: "${PACS_HTTP_PORT:?PACS_HTTP_PORT を指定してください}"
: "${PACS_DICOM_PORT:?PACS_DICOM_PORT を指定してください}"
: "${PACS_AE_TITLE:?PACS_AE_TITLE を指定してください}"
: "${PACS_DB_PASSWORD:?PACS_DB_PASSWORD を長いランダム値で指定してください}"
export PACS_BIND_ADDRESS
export PACS_HTTP_PORT
export PACS_DICOM_PORT
export PACS_AE_TITLE
export PACS_DB_PASSWORD
#############################################################################
# 1) 作業ディレクトリ作成(全て ao1 配下)
#############################################################################
echo "==== dcm4chee setup ===="
echo "ao1 = ${ao1}"
echo "PACS_BIND_ADDRESS= ${PACS_BIND_ADDRESS}"
echo "PACS_HTTP_PORT = ${PACS_HTTP_PORT}"
echo "PACS_DICOM_PORT = ${PACS_DICOM_PORT}"
echo "PACS_AE_TITLE = ${PACS_AE_TITLE}"
echo "StorageHost = ${StorageHost}"
echo "========================"
mkdir -p "${ao1}"/{ldap,slapd.d,db,wildfly,storage}
# プロトタイプで起動を優先した暫定設定。
# 診療環境では所有者・グループを確認し、必要最小限の権限へ変更すること。
chmod -R 777 "${ao1}"
#############################################################################
# 2) docker-compose.yml を作成
#############################################################################
cat > "${ao1}/docker-compose.yml" <<EOF_COMPOSE
version: "3.8"
services:
ldap:
image: dcm4che/slapd-dcm4chee:2.6.10-34.2
container_name: dcm4chee-ldap
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
# LDAP標準ポート。Docker内部だけで使用し、ホスト側へは公開しない。
expose:
- "389"
environment:
STORAGE_DIR: /storage/fs1
AE_TITLE: \${PACS_AE_TITLE}
TZ: Asia/Tokyo
volumes:
- /etc/localtime:/etc/localtime:ro
- ${ao1}/ldap:/var/lib/openldap/openldap-data
- ${ao1}/slapd.d:/etc/openldap/slapd.d
restart: unless-stopped
db:
image: dcm4che/postgres-dcm4chee:17.4-34
container_name: dcm4chee-db
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
# PostgreSQL標準ポート。Docker内部だけで使用し、ホスト側へは公開しない。
expose:
- "5432"
environment:
POSTGRES_DB: pacsdb
POSTGRES_USER: pacs
POSTGRES_PASSWORD: \${PACS_DB_PASSWORD}
TZ: Asia/Tokyo
volumes:
- /etc/localtime:/etc/localtime:ro
- ${ao1}/db:/var/lib/postgresql/data
restart: unless-stopped
arc:
image: dcm4che/dcm4chee-arc-psql:5.34.2
container_name: dcm4chee-arc
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
# コンテナ内部で使用する標準ポート。expose はホストやインターネットへ公開しない。
expose:
- "8080" # HTTP
- "8443" # HTTPS
- "9990" # WildFly管理系
- "9993" # WildFly管理系
- "11112" # DICOM
- "2762" # DICOM TLS等
- "2575" # DICOM関連
- "12575" # DICOM関連
# 右側はコンテナ内部の標準ポート。
# 左側の待受IP・ホスト側ポートは環境変数で指定し、実環境値を掲載しない。
# 必要最小限としてHTTPとDICOMだけを例示する。
ports:
- "\${PACS_BIND_ADDRESS}:\${PACS_HTTP_PORT}:8080"
- "\${PACS_BIND_ADDRESS}:\${PACS_DICOM_PORT}:11112"
environment:
POSTGRES_DB: pacsdb
POSTGRES_USER: pacs
POSTGRES_PASSWORD: \${PACS_DB_PASSWORD}
TZ: Asia/Tokyo
WILDFLY_WAIT_FOR: "ldap:389 db:5432"
WILDFLY_CHOWN: "/storage"
depends_on:
- ldap
- db
volumes:
- /etc/localtime:/etc/localtime:ro
- ${ao1}/wildfly:/opt/wildfly/standalone
- ${StorageHost}:/storage
restart: unless-stopped
EOF_COMPOSE
#############################################################################
# 3) 起動
#############################################################################
cd "${ao1}" || exit 1
echo "docker compose up -d を実行中..."
docker compose up -d
#############################################################################
# 4) 状態とログ
#############################################################################
docker ps
echo "----------------------------------------"
echo "dcm4chee-arc のログ(Ctrl+Cで終了)"
docker logs -f dcm4chee-arc
コード終了
PACS_NO_MOTO_A_END
PACS_NO_MOTO_B_BEGIN
PACSの素-b
用途:患者ID連携、およびdcm4cheeからOsiriXへの画像取得・表示
コード開始
#!/usr/bin/env bash
#############################################################################
# PACSの素-b:患者ID連携・OsiriX取得/表示(公開用参考版)
#
# 2025年12月時点のプロトタイプを、ホームページ掲載用に調整したものです。
# 実際の共有フォルダ、AE Title、院内IP、OsiriXサーバー名、患者IDは
# コード内に記載せず、実行環境の環境変数から受け取ります。
#
# 【重要】
# - 診療環境へそのまま導入せず、患者情報を含まない検証環境で確認してください。
# - OsiriX XML-RPC(標準例:8080)は、院内LANまたは管理されたVPN内だけで使用し、
# ルーターのポート転送などでインターネットへ直接公開しないでください。
# - FALLBACK_TEST_PATIENT_ID には、実在患者ではない検証専用IDを指定してください。
# - 本Composeには ports: を設けておらず、新たな待受ポートは公開しません。
# - PowerShellイメージは、本番利用時に動作確認済みの固定タグへ変更してください。
#
# 実行前に、利用環境側で次の値を設定します(値は公開しないでください)。
# export PSWATCH_HOST_DIR="/share/YOUR_SHARE/pswatch"
# export PACS_AE_TITLE="YOUR_AE_TITLE"
# export OSIRIX_HOST="YOUR_PRIVATE_LAN_HOST"
# export OSIRIX_SERVER_NAME="YOUR_OSIRIX_SERVER_NAME"
# export FALLBACK_TEST_PATIENT_ID="YOUR_NON_REAL_TEST_PATIENT_ID"
#
# 任意設定(標準例を既定値として残しています)。
# export OSIRIX_XMLRPC_PORT="8080"
# export DCM4CHEE_BASE_ARC="http://dcm4chee-arc:8080/dcm4chee-arc"
# export DCM4CHEE_DOCKER_NETWORK="dcm4chee_default"
# export POWERSHELL_IMAGE="mcr.microsoft.com/powershell:latest"
#############################################################################
set -Eeuo pipefail
umask 027
#############################################################################
# 0) 実環境固有値は環境変数で指定
#############################################################################
: "${PSWATCH_HOST_DIR:?PSWATCH_HOST_DIR を指定してください}"
: "${PACS_AE_TITLE:?PACS_AE_TITLE を指定してください}"
: "${OSIRIX_HOST:?OSIRIX_HOST を院内LANまたはVPN内のアドレスで指定してください}"
: "${OSIRIX_SERVER_NAME:?OSIRIX_SERVER_NAME を指定してください}"
: "${FALLBACK_TEST_PATIENT_ID:?FALLBACK_TEST_PATIENT_ID に実在患者ではない検証専用IDを指定してください}"
# 標準的な内部ポート/名称は参考情報として残し、必要に応じて環境側で変更します。
OSIRIX_XMLRPC_PORT="${OSIRIX_XMLRPC_PORT:-8080}"
DCM4CHEE_BASE_ARC="${DCM4CHEE_BASE_ARC:-http://dcm4chee-arc:8080/dcm4chee-arc}"
DCM4CHEE_DOCKER_NETWORK="${DCM4CHEE_DOCKER_NETWORK:-dcm4chee_default}"
POWERSHELL_IMAGE="${POWERSHELL_IMAGE:-mcr.microsoft.com/powershell:latest}"
ao2="${PSWATCH_HOST_DIR}"
# ポート値の単純な誤入力を防止します。
if ! [[ "${OSIRIX_XMLRPC_PORT}" =~ ^[0-9]+$ ]] \
|| (( 10#${OSIRIX_XMLRPC_PORT} < 1 || 10#${OSIRIX_XMLRPC_PORT} > 65535 )); then
echo "OSIRIX_XMLRPC_PORT は1〜65535の整数で指定してください。" >&2
exit 1
fi
export PSWATCH_HOST_DIR
export PACS_AE_TITLE
export OSIRIX_HOST
export OSIRIX_SERVER_NAME
export FALLBACK_TEST_PATIENT_ID
export OSIRIX_XMLRPC_PORT
export DCM4CHEE_BASE_ARC
export DCM4CHEE_DOCKER_NETWORK
export POWERSHELL_IMAGE
#############################################################################
# 1) フォルダ作成
#############################################################################
mkdir -p "${ao2}/id/sumi"
# 公開用参考版では chmod -R 777 を自動実行しません。
# QNAPの所有者・グループ・共有フォルダACLを確認し、必要最小限の権限を設定してください。
#############################################################################
# 2) docker-compose.yml 生成
#############################################################################
# シングルクォート付きヒアドキュメントにより、ここでは実環境値を展開しません。
# docker compose 実行時に、上でexportした環境変数が置換されます。
cat > "${ao2}/docker-compose.yml" <<'YAML'
services:
pswatch:
image: "${POWERSHELL_IMAGE:-mcr.microsoft.com/powershell:latest}"
container_name: pswatch
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
environment:
TZ: Asia/Tokyo
WATCH_ROOT: "/app/id"
# dcm4cheeコンテナ内部のHTTP標準例8080。ホスト側への公開設定ではありません。
BASE_ARC: "${DCM4CHEE_BASE_ARC:-http://dcm4chee-arc:8080/dcm4chee-arc}"
# 実環境固有値はコードに埋め込まず、環境変数から受け取ります。
AETITLE: "${PACS_AE_TITLE:?PACS_AE_TITLE is required}"
MAC_HOST: "${OSIRIX_HOST:?OSIRIX_HOST is required}"
# OsiriX XML-RPCの標準例。院内LAN/VPN内だけで使用してください。
MAC_PORT: "${OSIRIX_XMLRPC_PORT:-8080}"
SERVER_NAME: "${OSIRIX_SERVER_NAME:?OSIRIX_SERVER_NAME is required}"
# 実在患者IDを公開コードへ記載しません。検証専用の架空IDだけを指定します。
FALLBACK_PATIENT_ID: "${FALLBACK_TEST_PATIENT_ID:?FALLBACK_TEST_PATIENT_ID is required}"
volumes:
- "${PSWATCH_HOST_DIR:?PSWATCH_HOST_DIR is required}/id:/app/id:rw"
# スクリプト側は読み取り専用とし、入力/完了フォルダだけを書き込み可能にします。
- "${PSWATCH_HOST_DIR:?PSWATCH_HOST_DIR is required}:/app/script:ro"
working_dir: /app/script
command: ["pwsh", "-File", "watch.ps1"]
# ports: は設定しません。このコンテナ自体の待受ポートをホストへ公開しません。
networks:
default:
external: true
name: "${DCM4CHEE_DOCKER_NETWORK:-dcm4chee_default}"
YAML
# 生成ファイルにはホスト側パス等が含まれるため、一般ユーザーから読めない権限にします。
chmod 0640 "${ao2}/docker-compose.yml"
#############################################################################
# 3) 共通 watch.ps1 保存
#############################################################################
cat > "$ao2/watch.ps1" <<'PS1'
#############################################################################
# 共通 watch.ps1(Windows / QNAP / Linux / Mac 全対応)
# 設定はすべて環境変数から取得(WATCH_ROOT / BASE_ARC / AETITLE / etc)
# 公開用参考版:標準出力へ患者ID、StudyInstanceUID、入力ファイル名を表示しない
#############################################################################
# ===============================================
# 1) 必須環境変数チェック
# ===============================================
$requiredEnv = @(
"WATCH_ROOT",
"BASE_ARC",
"AETITLE",
"MAC_HOST",
"MAC_PORT",
"SERVER_NAME",
"FALLBACK_PATIENT_ID"
)
foreach ($name in $requiredEnv) {
$value = [Environment]::GetEnvironmentVariable($name)
if ([string]::IsNullOrWhiteSpace($value)) {
throw "環境変数 $name が未設定です"
}
}
# ===============================================
# 2) 監視パスと完了フォルダ生成
# ===============================================
$watch = $env:WATCH_ROOT
$done = Join-Path $watch "sumi"
if (-not (Test-Path $watch)) { New-Item $watch -ItemType Directory -Force | Out-Null }
if (-not (Test-Path $done )) { New-Item $done -ItemType Directory -Force | Out-Null }
# ===============================================
# 3) dcm4chee / OsiriX 用設定まとめ
# dcm4cheeおよびOsiriXへの通信は、院内LANまたは管理されたVPN内に限定すること。
# ===============================================
$cfg = @{
baseArc = $env:BASE_ARC
hdr = @{ Accept = 'application/dicom+json' }
AET = $env:AETITLE
MacHost = $env:MAC_HOST
Port = $env:MAC_PORT
Srv = $env:SERVER_NAME
Done = $done
Fallback = $env:FALLBACK_PATIENT_ID
}
# ===============================================
# 4) CSV行パース(ID,漢字,カナ,性別,誕生日)
# ===============================================
function Parse-PatientLine {
param([string]$Raw)
if (-not $Raw) { return $null }
$cols = $Raw -split ','
if ($cols.Count -lt 5) { return $null }
$id = $cols[0].Trim()
$kana = $cols[2].Trim()
$sex = $cols[3].Trim()
$birth = $cols[4].Trim()
if (-not $id) { return $null }
# 性別 1=M 2=F
switch ($sex) {
'1' { $sex = 'M' }
'2' { $sex = 'F' }
default { $sex = $null }
}
# YYYY/MM/DD → YYYYMMDD
$birthDA = $null
if ($birth) {
$digits = ($birth -replace '\D','')
if ($digits.Length -eq 8) { $birthDA = $digits }
}
# カナ → PN(姓^名)
$pnAlphabetic = if ($kana) { $kana -replace '\s+', '^' } else { "" }
[PSCustomObject]@{
PatientID = $id
PNAlphabetic = $pnAlphabetic
Sex = $sex
BirthDA = $birthDA
}
}
# ===============================================
# 5) ファイル監視開始 *.txt
# ===============================================
$fsw = New-Object IO.FileSystemWatcher $watch,'*.txt'
$fsw.IncludeSubdirectories = $false
$fsw.EnableRaisingEvents = $true
Register-ObjectEvent $fsw Created -MessageData $cfg -Action {
Start-Sleep -Milliseconds 200
$f = $Event.SourceEventArgs.FullPath
if (-not (Test-Path $f)) { return }
$c = $Event.MessageData
try {
#############################################################
# 入力解析
#############################################################
$raw = Get-Content $f -Raw -Encoding "Shift-JIS"
$d = Parse-PatientLine $raw
if (-not $d) { throw "IDなし" }
$id = $d.PatientID
$pn = $d.PNAlphabetic
$sex = $d.Sex
$bd = $d.BirthDA
#############################################################
# QIDO で既存患者検索
# - PatientID で検索
# - Issuer / PN / BD / Sex を取得
#############################################################
$IssuerID = $null
$exPN = $null
$exBD = $null
$exSex = $null
$existingList = @()
try {
$pidEnc = [uri]::EscapeDataString($id)
$uri = "$($c.baseArc)/aets/$($c.AET)/rs/patients?" +
"00100020=$pidEnc" + # PatientID
"&includefield=00100021" + # Issuer
"&includefield=00100010" + # PN
"&includefield=00100030" + # BirthDate
"&includefield=00100040" # Sex
$res = Invoke-RestMethod -Uri $uri -Headers $c.hdr -EA Stop
if ($res) {
if ($res -isnot [System.Array]) { $existingList = @($res) }
else { $existingList = $res }
}
}
catch {
# QIDO 失敗時は「既存なし」とみなす
$existingList = @()
}
$existingCount = $existingList.Count
#############################################################
# 既存患者情報(先頭1件)を取り出し
#############################################################
if ($existingCount -ge 1) {
$p = $existingList[0]
if ($p.'00100021' -and $p.'00100021'.Value) {
$IssuerID = $p.'00100021'.Value[0]
}
if ($p.'00100010' -and $p.'00100010'.Value) {
$exPN = $p.'00100010'.Value[0].Alphabetic
}
if ($p.'00100030' -and $p.'00100030'.Value) {
$exBD = $p.'00100030'.Value[0]
}
if ($p.'00100040' -and $p.'00100040'.Value) {
$exSex = $p.'00100040'.Value[0]
}
}
#############################################################
# 可変部の差分チェック
#############################################################
$hasChange = $false
if ($existingCount -ge 1) {
if ($pn -and ($pn -ne $exPN )) { $hasChange = $true }
if ($bd -and ($bd -ne $exBD )) { $hasChange = $true }
if ($sex -and ($sex -ne $exSex)) { $hasChange = $true }
}
# ★ここで必ず true で初期化(デフォルトは POST する)
$doPost = $true
#############################################################
# 分岐:
# 1) 既存なし(0件) → 新規作成 (Issuerなし)
# 2) 既存あり(1件以上) & 差分なし → 何もしない
# 3) 既存あり(1件以上) & 差分あり → Issuer再利用して POST
#############################################################
# 2) 既存が1件以上いて、かつ可変部に変更なし → POST だけスキップ
if ($existingCount -ge 1 -and -not $hasChange) {
Write-Host "pswatch | 既存 Patient に変更なし → POST スキップ"
$doPost = $false # ★ return せず、このあとリトリーブ等は続行
}
if ($doPost) {
#############################################################
# 1) / 3) 共通:POST Upsert
# - 新規 (existingCount=0) なら Issuerは付けない
# - 既存あり (existingCount>=1) なら、既存 Issuer があれば付ける
#############################################################
$payload = @(@{
"00080005" = @{ vr="CS"; Value=@("ISO_IR 192") }
"00100020" = @{ vr="LO"; Value=@($id) }
"00100010" = @{ vr="PN"; Value=@(@{ Alphabetic=$pn }) }
})
if ($bd ) { $payload[0]."00100030" = @{ vr="DA"; Value=@($bd ) } }
if ($sex) { $payload[0]."00100040" = @{ vr="CS"; Value=@($sex) } }
# 既存が1件以上ある場合のみ、Issuer を再利用して付ける
if ($existingCount -ge 1 -and $IssuerID) {
$payload[0]."00100021" = @{ vr="LO"; Value=@($IssuerID) }
}
$bodyJson = $payload | ConvertTo-Json -Depth 8
Invoke-WebRequest "$($c.baseArc)/aets/$($c.AET)/rs/patients" `
-Method POST -ContentType 'application/dicom+json' `
-Headers @{ Accept='application/json' } `
-Body $bodyJson `
-TimeoutSec 30 -EA Stop | Out-Null
}
#############################################################
# 最新 Study(MR → Any → fallback)
#############################################################
$pidEnc = [uri]::EscapeDataString($id)
$uri = "$($c.baseArc)/aets/$($c.AET)/rs/studies?00100020=$pidEnc" +
"&includefield=0020000D&includefield=00080061" +
"&orderby=-StudyDate,-StudyTime&limit=20"
$rs = Invoke-RestMethod -Uri $uri -Headers $c.hdr -EA SilentlyContinue
$uidAny = if ($rs) { $rs[0].'0020000D'.Value[0] } else { $null }
$mr = $rs | Where-Object { $_.'00080061'.Value -contains 'MR' } | Select-Object -First 1
$uidMR = if ($mr) { $mr.'0020000D'.Value[0] } else { $null }
if ($uidMR) {
$k = "StudyInstanceUID"; $v = $uidMR
}
elseif ($uidAny) {
$k = "StudyInstanceUID"; $v = $uidAny
}
else {
$k = "PatientID"; $v = $c.Fallback
}
#############################################################
# OsiriX 20.50 専用:まず Retrieve → 1秒待つ → DisplayStudy
#############################################################
# --- Retrieve ----------------------------------------------------------
$xmlRetrieve = @"
<?xml version="1.0"?>
<methodCall>
<methodName>Retrieve</methodName>
<params>
<param><value><struct>
<member><name>serverName</name><value><string>$($c.Srv)</string></value></member>
<member><name>filterKey</name><value><string>$k</string></value></member>
<member><name>filterValue</name><value><string>$v</string></value></member>
<member><name>then</name><value><string>open</string></value></member>
<member><name>retrieveOnlyIfNeeded</name><value><string>yes</string></value></member>
</struct></value></param>
</params>
</methodCall>
"@
try {
Invoke-WebRequest ("http://{0}:{1}" -f $c.MacHost,$c.Port) `
-Method POST -ContentType "text/xml; charset=utf-8" `
-Body $xmlRetrieve -TimeoutSec 30 -EA Stop | Out-Null
Write-Host "pswatch | [OK] Retrieve sent: filter=$k"
}
catch {
$errType = $_.Exception.GetType().Name
Write-Host "pswatch | [ERR] Retrieve failed ($errType)"
}
# --- Wait (必須:OsiriX の DB が落ち着くまで) -------------------------
Start-Sleep -Seconds 1
# --- DisplayStudy ------------------------------------------------------
if ($k -eq "StudyInstanceUID") {
# StudyInstanceUID 指定
$xmlDisplay = @"
<?xml version="1.0"?>
<methodCall>
<methodName>DisplayStudy</methodName>
<params>
<param><value><struct>
<member><name>studyInstanceUID</name><value><string>$v</string></value></member>
</struct></value></param>
</params>
</methodCall>
"@
}
else {
# PatientID 指定
$xmlDisplay = @"
<?xml version="1.0"?>
<methodCall>
<methodName>DisplayStudy</methodName>
<params>
<param><value><struct>
<member><name>patientID</name><value><string>$v</string></value></member>
</struct></value></param>
</params>
</methodCall>
"@
}
try {
Invoke-WebRequest ("http://{0}:{1}" -f $c.MacHost,$c.Port) `
-Method POST -ContentType "text/xml; charset=utf-8" `
-Body $xmlDisplay -TimeoutSec 10 -EA Stop | Out-Null
Write-Host "pswatch | [OK] DisplayStudy opened: filter=$k"
}
catch {
$errType = $_.Exception.GetType().Name
Write-Host "pswatch | [ERR] DisplayStudy failed ($errType)"
}
#############################################################
# ファイル移動
#############################################################
Move-Item $f (Join-Path $c.Done (Split-Path $f -Leaf)) -Force
Write-Host "pswatch | 処理済みファイルを完了フォルダへ移動"
}
catch {
$errType = $_.Exception.GetType().Name
Write-Host "pswatch | 処理エラー ($errType)"
try { Move-Item $f (Join-Path $c.Done (Split-Path $f -Leaf)) -Force } catch {}
}
} | Out-Null
Write-Host "pswatch | 監視開始 — Ctrl+C で停止"
while ($true) { Wait-Event -Timeout 1 | Out-Null }
PS1
chmod 0640 "${ao2}/watch.ps1"
#############################################################################
# 4) Docker 起動(絶対パス指定)
#############################################################################
docker compose -f "$ao2/docker-compose.yml" up -d
docker ps --filter "name=^/pswatch$"
docker logs -f pswatch
コード終了
PACS_NO_MOTO_B_END
【追記:OsiriX MD版】
PACSの素-bの「OsiriX 20.50専用:まず Retrieve → 1秒待つ → DisplayStudy」から「ファイル移動」の直前までを、下記へ置き換えてください。
ご提示のMD版ではDisplayStudyと1秒待機を使用せず、Retrieveのthenをopenにします。その他の部分は変更不要です。
#############################################################
# OsiriX MD版 XML-RPC
#############################################################
$xml = @"
<?xml version="1.0"?>
<methodCall>
<methodName>Retrieve</methodName>
<params>
<param><value><struct>
<member><name>serverName</name><value><string>$($c.Srv)</string></value></member>
<member><name>filterKey</name><value><string>$k</string></value></member>
<member><name>filterValue</name><value><string>$v</string></value></member>
<member><name>then</name><value><string>open</string></value></member>
<member><name>retrieveOnlyIfNeeded</name><value><string>yes</string></value></member>
</struct></value></param>
</params>
</methodCall>
"@
Invoke-WebRequest ("http://{0}:{1}" -f $c.MacHost,$c.Port) `
-Method POST -ContentType "text/xml; charset=utf-8" `
-Body $xml -TimeoutSec 30 -EA Stop | Out-Null





