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