219 lines
7.8 KiB
Markdown
219 lines
7.8 KiB
Markdown
# 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)
|
||
|
||
```bash
|
||
# 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)
|
||
|
||
```bash
|
||
/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)
|
||
|
||
```bash
|
||
# 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
|
||
|
||
```bash
|
||
# 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:**
|
||
```php
|
||
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):**
|
||
```php
|
||
// 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:**
|
||
```php
|
||
$cmd = "/usr/bin/dcmsend +sd +rd 172.16.0.120 104 $dicomdir/DICOMDIR";
|
||
```
|
||
|
||
**Replace with:**
|
||
```php
|
||
$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):**
|
||
```php
|
||
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):**
|
||
```php
|
||
// 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
|
||
|
||
```bash
|
||
# 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
|
||
```
|