QNAP Dockerにdcm4chee PACSを構築する方法|Docker ComposeコードをAIで生成 | 名古屋~春日井の脳神経外科 | 勝川脳神経クリニック

  • 電話でのお問い合わせ
  • メールでのお問い合わせ 勝川脳神経クリニック
  • 勝川脳神経クリニック 交通アクセス
  • 脳ドック
  • メールでのお問い合わせ 勝川脳神経クリニック
  • 電話でのお問い合わせ
  • 勝川脳神経クリニック 交通アクセス

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-aPACS-b
PACS-adcm4chee
PACS-bIDdcm4cheeOsiriX

PACS_NO_MOTO_A_BEGIN
PACS-a
dcm4chee

#!/usr/bin/env bash
#############################################################################
# dcm4cheeldap + postgres + arc Docker bash
# - QNAP QuTS hero / ZFS
# - a1 dcm4chee
# - docker compose 使
#
#
# -
# - IPAE TitleDB
#
# - LDAPPostgreSQL
#############################################################################

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
# LANPACS_BIND_ADDRESS NASLANIP
# 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"
# LDAPDocker使
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"
# PostgreSQLDocker使
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
# HTTPDICOM
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
IDdcm4cheeOsiriX

#!/usr/bin/env bash
#############################################################################
# PACS-bIDOsiriX
#
# 202512調
# AE TitleIPOsiriXID
#
#
#
# -
# - OsiriX XML-RPC8080LANVPN使
#
# - 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 LANVPN}"
: "${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 165535" >&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
# QNAPACL

#############################################################################
# 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"

# dcm4cheeHTTP8080
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-RPCLANVPN使
MAC_PORT: "${OSIRIX_XMLRPC_PORT:-8080}"

SERVER_NAME: "${OSIRIX_SERVER_NAME:?OSIRIX_SERVER_NAME is required}"

# IDID
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.ps1Windows / QNAP / Linux / Mac
# WATCH_ROOT / BASE_ARC / AETITLE / etc
# IDStudyInstanceUID
#############################################################################

# ===============================================
# 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
# dcm4cheeOsiriXLANVPN
# ===============================================
$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) CSVID,,,,
# ===============================================
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) } }

# 1Issuer
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
}

#############################################################
# StudyMR 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-bOsiriX 20.50 Retrieve 1 DisplayStudy
MDDisplayStudy1使Retrievethenopen

#############################################################
# 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

Information