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

219 lines
7.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```