Files
dicom-iso/todo/mkiso-replace-java-with-dcmtk.detail.md
2026-06-05 08:11:44 +07:00

7.8 KiB
Raw Permalink Blame History

mkiso dcmtk Replacement — Implementation Details

Core Architectural Change

dcm4che2's dcmqr is a monolithic tool: one process handles the storage SCP, C-MOVE SCU, query, AND file writing.
dcmtk separates these into two processes that must run concurrently:

┌─────────────────────────────────────────┐
│  dcm4che2 dcmqr (single process)        │
│  ┌─────────────────────────────────────┐│
│  │ Storage SCP (listen CDRECORD:10104) ││
│  │ C-MOVE SCU                         ││
│  │ File writer (cstoredest)          ││
│  └─────────────────────────────────────┘│
└─────────────────────────────────────────┘

┌──────────────┐    ┌──────────────┐
│  storescp    │    │  movescu     │
│  AE:CDRECORD │    │  AE:CDRECORD │
│  port:10104  │    │  -aem CDRECORD│
│  -od DICOMDIR│    │  +P 10104    │
└──────┬───────┘    └──────┬───────┘
       │                   │
       │  receives         │  sends C-MOVE-RQ
       │  C-STORE          │  to PACS
       ▼                   ▼
   ┌─────────────────────────────┐
   │   PACS (ABPACS:11112)       │
   └─────────────────────────────┘

Command Templates

1. storescp (background receiver)

# Start storage SCP in background, capture PID
/data/dcmtk-bin/storescp 10104 \
  -aet CDRECORD \
  -od "${dicomdir}/DICOMDIR" \
  +xa \
  &> /tmp/storescp_${uniq}.log &

STORESCP_PID=$!

# Give storescp time to bind the port
sleep 1

Key flags:

Flag Purpose
10104 Listen port (must match what PACS sends to)
-aet CDRECORD AE title (must match C-MOVE destination AE)
-od DICOMDIR Output directory for received files
+xa Accept all transfer syntaxes (max compatibility)

2. movescu (C-MOVE initiator) — Simplified (no modality loop)

/data/dcmtk-bin/movescu \
  -aet CDRECORD \
  -aec ABPACS \
  -aem CDRECORD \
  localhost 11112 \
  -S \
  -k 0008,0050="${accession_number}" \
  -k 0010,0020="" \
  --no-port

Key flags:

Flag Purpose
-aet CDRECORD Our AE title (calling)
-aec ABPACS PACS AE title (called)
-aem CDRECORD Move destination AE (tells PACS to C-STORE to CDRECORD)
localhost 11112 PACS host:port
-S Study Root query model
-k 0008,0050=X Accession Number query key
-k 0010,0020="" Patient ID = wildcard (required for Study Root)
--no-port No incoming port on movescu (storescp handles incoming)

3. movescu — Conservative (with modality loop, replicating original behavior)

# Same as above but add modality filter via query key
/data/dcmtk-bin/movescu \
  -aet CDRECORD \
  -aec ABPACS \
  -aem CDRECORD \
  localhost 11112 \
  -S \
  -k 0008,0050="${accession_number}" \
  -k 0010,0020="" \
  -k 0008,0060="${modality_code}" \
  --no-port

Note: -k 0008,0060=MR filters queries to studies with Modality=MR.
This is NOT identical to dcm4che2's -cstore MR (which filters at the association/presentation-context level).
For most PACS, the query-level filter is sufficient. If the PACS requires specific presentation context negotiation per modality, the simplified approach (no modality loop) with +xa (accept all) is recommended.

4. Cleanup

# After movescu completes (or times out):
kill $STORESCP_PID 2>/dev/null
wait $STORESCP_PID 2>/dev/null

Per-File Replacements

mkiso.php — current Java block (lines ~56-59)

Current:

foreach($modalities as $cstore=>$v) {
   $cmd = "JAVA_HOME=/usr/lib/jvm/jdk1.8.0_144 LANG=en_US.iso-8859-1 /usr/local/dcm4che/dcm4che2/bin/dcmqr -L CDRECORD:10104 ABPACS@localhost:11112 -cmove CDRECORD -qAccessionNumber=${accession_number} -cstore $cstore -cstoredest $dicomdir/DICOMDIR";
   exec($cmd, $outputRes);
}

Replace with (simplified approach):

// Start storescp as background storage receiver
$storescp_pid = exec("/data/dcmtk-bin/storescp 10104 -aet CDRECORD -od ${dicomdir}/DICOMDIR +xa &> /tmp/storescp_" . basename($dicomdir) . ".log & echo $!");

// Allow storescp to bind port
sleep(1);

// Single C-MOVE for the accession number (Study Root model)
$cmd = "/data/dcmtk-bin/movescu -aet CDRECORD -aec ABPACS -aem CDRECORD localhost 11112 -S -k 0008,0050=${accession_number} -k 0010,0020= --no-port";
exec($cmd, $outputRes, $exitCode);

// Clean up storescp
exec("kill $storescp_pid 2>/dev/null");

mkiso2.php — current Java block (lines ~56-59)

Same C-MOVE replacement as mkiso.php.

Additionally, replace dcmsend (line ~62):

Current:

$cmd = "/usr/bin/dcmsend +sd +rd 172.16.0.120 104 $dicomdir/DICOMDIR";

Replace with:

$cmd = "/data/dcmtk-bin/storescu -aet CDRECORD +sd +r 172.16.0.120 104 ${dicomdir}/DICOMDIR";

/usr/bin/dcmsend does not exist on the current system. storescu +sd +r (scan directories + recurse) is the dcmtk equivalent.

mkiso_multiple.php — current Java block (lines ~80-87)

Current (nested loop):

foreach($modalities as $cstore=>$v) {
   foreach($as as $accession_number) {
      $cmd = "JAVA_HOME=... dcmqr ... -qAccessionNumber=${accession_number} -cstore $cstore -cstoredest $dicomdir/DICOMDIR";
      exec($cmd, $outputRes);
   }
}

Replace with (simplified, one C-MOVE per accession):

// Start storescp once for all accessions
$storescp_pid = exec("/data/dcmtk-bin/storescp 10104 -aet CDRECORD -od ${dicomdir}/DICOMDIR +xa &> /tmp/storescp_" . basename($dicomdir) . ".log & echo $!");
sleep(1);

foreach($as as $accession_number) {
   $cmd = "/data/dcmtk-bin/movescu -aet CDRECORD -aec ABPACS -aem CDRECORD localhost 11112 -S -k 0008,0050=${accession_number} -k 0010,0020= --no-port";
   exec($cmd, $outputRes, $exitCode);
}

exec("kill $storescp_pid 2>/dev/null");

DICOM Tag Reference

Tag Name Used As
(0008,0050) Accession Number Query key — filters by accession
(0008,0052) Query/Retrieve Level Set by -S (Study) — queries at study level
(0008,0060) Modality Query key — filters by modality (if using conservative approach)
(0010,0020) Patient ID Required wildcard for Study Root Q/R

Error Handling Considerations

  1. storescp already bound: If 10104 is in use, storescp will fail. Use a random port or check first.
  2. movescu timeout: Default is unlimited. Consider -to 300 (5 min timeout) for production.
  3. PACS unreachable: movescu returns non-zero exit code — PHP should handle this (currently scripts have no error handling).
  4. Partial retrieval: movescu may return success even if some images fail. The exit code reflects the C-MOVE response status.
  5. Race condition: The sleep 1 is a heuristic. For production, consider polling to check storescp is ready.

Verification Commands

# Test storescp starts correctly
/data/dcmtk-bin/storescp 10104 -aet CDRECORD -od /tmp/test_dicom +xa &
PID=$!
sleep 1
echo "storescp PID: $PID (should be running)"
kill $PID

# Test movescu against PACS (dry run verification)
/data/dcmtk-bin/movescu -aet CDRECORD -aec ABPACS -aem CDRECORD localhost 11112 \
  -S -k "0008,0050=MR.180505.026" -k "0010,0020=" --no-port -v

# Test storescu (replacement for dcmsend)
/data/dcmtk-bin/storescu -aet CDRECORD +sd +r 172.16.0.120 104 /tmp/test_dicom/ -v