Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rust-dicom-station"
version = "0.6.0"
version = "0.6.5"
edition = "2021"
description = "Fast, robust DICOM / RT DICOM viewer in pure Rust (CT/MR volumes, RTSTRUCT, RTDOSE, RTPLAN) with a three-view MPR layout"
license = "MIT"
Expand Down
13 changes: 9 additions & 4 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,19 @@ rust-dicom-station
├── Application (GUI, egui over wgpu)
│ ├── Window chrome: menu bar, toolbar (W/L, presets, 3D, crosshair, reset), status bar
│ ├── Side panel: registration, simulation, per-dataset sections
│ │ (series tree, structures, segmentations, dose + isodose, plans, planar images,
│ │ spatial registrations, treatment records, warnings)
│ ├── Side panel: registration, simulation, and per dataset a DICOM tree —
│ │ patient ▶ study ▶ modality (CT/MR/US…) ▶ series, with RT structures,
│ │ segmentations, dose and plans inside their own study — plus the
│ │ dataset-level dose display, planar images, spatial registrations,
│ │ treatment records and warnings
│ ├── Views: 1 × 3 or 2 × 3 (comparison) linked MPR viewports, crosshair,
│ │ zoom / pan / W-L interaction, maximize, per-view caches
│ ├── Floating windows: 3D structures (both datasets through the registration,
│ │ per-dataset opacity, vector-field glyphs), planar image viewers
│ ├── Data tree operations: rename every level (patient, study, series, sets,
│ │ structures, segments, dose, plan, planar, REG, records);
│ │ Shift-click range selection; copy / move / remove / export the ticked
│ │ items from a button row or the context menu;
│ │ copy / move / remove patient · study · series across datasets;
│ │ create / connect / copy / move / remove RT structure sets and segmentation
│ │ series; copy / move / remove single or selected structures and segments
Expand Down Expand Up @@ -82,7 +86,8 @@ rust-dicom-station
│ ├── Known-transform study generator (rigid + Gaussian deformation, registration QA)
│ └── Digitally reconstructed radiographs: exact Siddon ray tracing (plastimatch)
│ and interpolating ray-casting (ITK), IEC cone-beam geometry, beam's-eye view
│ from an RTPLAN beam, side-by-side difference
│ from an RTPLAN beam, side-by-side difference, filed into the data tree
│ as planar RT images with the geometry that produced them
├── Image registration
│ ├── elastix-style rigid (6-DOF Euler, ASGD, pyramids, stochastic sampling)
Expand Down
25 changes: 25 additions & 0 deletions docs/drr.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,31 @@ The two renderings are shown side by side with a shared display window
radiographs are usually read dark-on-light — and a **Difference** view that
maps the signed difference blue↔red about a grey zero.

## Into the data tree

*➕ Add to dataset A/B* files the current rendering (or both, when the two
projectors were run together) under **Planar images** in that dataset's
tree. A DRR *is* an RT Image, so once it is one it inherits everything the
tree already does: its own viewer window with window/level and the correct
physical aspect ratio, renaming, and travelling with the dataset when it is
copied or moved.

The geometry that produced it rides along as the info rows the planar viewer
lists — engine, SAD/SID, gantry and couch angles, isocentre, panel size, HU
model, threshold, sampling step (ray-cast only) and render time — so a
radiograph that has been sitting in the tree for an hour can still say
exactly what it is. Labels are `DRR Siddon · G 90° C 0°` and are made unique
on the way in, because rendering the same geometry twice is what one does
while tuning it.

Whichever greyscale the window is showing is what gets stored: with
**Invert** on (the default) the values are mirrored about the middle of the
range so dark is high attenuation, as on a radiograph. The range itself is
unchanged either way, and the info rows say which convention was used.

Planar images are viewer-side objects: they are not written by
*File ▶ Export dataset*, which covers CT, RTSTRUCT, SEG, RTDOSE and RTPLAN.

## Where it fits

DRR generation is a *simulation* feature, not a registration one: it shares
Expand Down
2 changes: 1 addition & 1 deletion docs/registration.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ field and tolerate inconsistent pairs.

Landmarks are placed from the interface: put the crosshair on the same
anatomy in both datasets and press **➕ Add pair** in the *Landmarks* section
(turn *View ▶ Link crosshairs* off first, or both crosshairs move together).
(turn *View ▶ Sync crosshairs* off first, or both crosshairs move together).
Each pair shows its displacement, and after a run, its residual.

## Running a registration
Expand Down
6 changes: 4 additions & 2 deletions docs/segmentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ stays intact and simply reports that it is not editable here.
The sidebar *Segmentations* section shows the series as tree nodes and,
below the active one, its segments: visibility, color, active selection,
volume in cm³, per-stroke undo (**Ctrl+Z**), delete, and conversion to
RTSTRUCT (**→RS**, below). *All* / *None* tick every segment or none, and
the tick doubles as the selection the right-click actions work on — see
RTSTRUCT (**→RS**, below). *All* / *None* tick every segment or none,
Shift-click extends a range, and the tick doubles as the selection that the
row of actions beneath it — copy, move, remove, and *💾* to export just
those segments as a DICOM SEG file — works on. See
[viewer.md](viewer.md#structures-and-segmentations-in-the-tree).

## Tools
Expand Down
110 changes: 88 additions & 22 deletions docs/viewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,22 @@ oblique acquisitions display consistently but the plane names are nominal —
the anatomical edge labels (L/R/A/P/S/I) always reflect the true patient
directions derived from the direction cosines.

The views tile the central area edge to edge — no gaps, no frames — and
each one carries its own **slice scrubber** along its bottom edge, drawn
over the image rather than in a strip beneath it. The plane and dataset
name in the top-left corner is white in every view; the anatomical edge
labels keep their own colour.

Each viewport carries two corner buttons (both name themselves on hover):
**⟲** resets that view's zoom and pan and puts the crosshair back at the
volume center — which returns that dataset's three views to their central
slices — and **⛶ / ❐** maximizes the view to fill the window and restores
the layout again. The toolbar holds a global **⟲** (the same reset for
every view of both datasets), the **⌖** crosshair toggle (while hidden,
left-click navigation is disabled entirely and slices change only by
scrolling), the **3D A / 3D B** buttons and the segmentation tools.
scrolling), the **🔗** crosshair-sync toggle next to it (shown while the
crosshair is on, active with two datasets loaded), the **3D A / 3D B**
buttons and the segmentation tools.

**Window/level.** Right-drag on any view adjusts interactively
(x = width, y = center); the toolbar offers the numeric fields and the
Expand All @@ -73,8 +81,10 @@ Window/level is shared between datasets A and B so both CTs are windowed
identically.

**Status bar.** Patient coordinates, voxel indices, HU and dose (Gy and %
of the reference dose) at the crosshair; in comparison mode the readouts
for A and B are shown side by side.
of the reference dose) at the crosshair; in comparison mode both datasets
report the full set side by side, each at its own crosshair. The mouse
bindings of the tool in force are behind the **?** at the right end — hover
it to read them.

**The left panel.** *View ▶ Left panel*, the **F9** key and the arrow on
the window's left edge hide and show it; dragging its inner edge past the
Expand Down Expand Up @@ -107,17 +117,48 @@ accumulated from any number of folders. *File ▶ Add DICOM folder to A/B…*
merges a scanned folder into the slot without unloading what is already
there; duplicates (by UID) are skipped and reported.

The left panel shows each dataset as a **Data tree** — a full DICOM
hierarchy: patient
(PatientName/PatientID) ▶ study (StudyInstanceUID, with date and
description) ▶ image series — with the displayed series marked; clicking
another series loads it. Long names, descriptions and IDs wrap over as
many lines as they need, so the panel can be dragged narrow without
cutting them off; only the section headers stay on one line. The standard reference chain is parsed and shown
as links: each structure set displays the image series its contours were
drawn on (RTReferencedSeriesSequence), each dose the plan it was computed
for (ReferencedRTPlanSequence), and each plan the structure set it was
created on (ReferencedStructureSetSequence).
The left panel shows each dataset as a full DICOM hierarchy:

```
Dataset A
└ Doe John (P1) patient — PatientName / PatientID
└ Study 20260827 — Planning study — StudyInstanceUID, date, description
├ CT (2) modality
│ ├ chest (120 sl.) image series
│ └ abdomen (90 sl.)
├ MR (1)
├ RT structures (12/12)
│ └ ▣ Approved (12 ROIs) ▶ CT chest
├ Segmentations (8/8)
│ └ ✎ TotalSeg (8 segments) ▶ CT chest
├ Dose (1)
└ Plan: IMRT
Dose display · Planar images · Spatial registrations · Records · Warnings
```

The modality level (CT / MR / US / PT …) is one DICOM implies but does not
store as a node; it is grouped from the series' Modality, in first-seen
order. Everything that carries a StudyInstanceUID — image series, RT
structure sets, segmentation series, dose grids and plans — sits inside the
study it belongs to. An RT object whose StudyInstanceUID is blank or names
a study that is not loaded is filed under the study of the image series it
references, and failing that under the first study, because an object that
cannot be reached is worse than one shown a level from where its header
claims it lives.

What is left below the tree is what has no study to sit under: planar
images carry no study link at all, spatial registrations and treatment
records belong to a frame of reference rather than a study, and **Dose
display** — colorwash, isodose ladder, opacity, threshold — is one setting
shared by both datasets, so it is shown once.

The displayed series is marked; clicking another loads it. Long names,
descriptions and IDs wrap over as many lines as they need, so the panel can
be dragged narrow without cutting them off. The standard reference chain is
parsed and shown as links: each structure set and segmentation series
displays the image series it is drawn on, each dose the plan it was
computed for (ReferencedRTPlanSequence), and each plan the structure set it
was created on (ReferencedStructureSetSequence).

**Right-clicking** any level of the tree — patient, study or series —
opens a context menu to **rename**, **copy**, **move** or **remove** it. Copy/move
Expand Down Expand Up @@ -152,16 +193,38 @@ to the displayed image series. **Right-clicking a series node** offers:
* *✎ Rename series…*.

Each item's **check box is both its visibility and its selection**, so
*All* / *None* tick everything or nothing and the right-click actions
operate on whatever is ticked. **Right-clicking a structure or segment**
offers:
*All* / *None* tick everything or nothing and the actions operate on
whatever is ticked. **Shift-click** a check box to tick — or untick — the
whole range from the last one you clicked: the span is filled with the
clicked row's new value, and rows outside it are never touched, because the
box is a visibility toggle as much as a selection and silently hiding
structures you did not point at would be worse than the convenience.

One row carries the lot: for structures **All · None · Copy to · Move to ·
🗑 · *n* selected**, and for segmentations **New · All · None · Copy to ·
Move to · 🗑 · 💾 · *n* selected**. *Copy to* and *Move to* open the same
destination submenu described below; **💾** writes just the ticked segments
as a DICOM SEG file of their own. The buttons grey out when nothing is
ticked.

The per-row buttons a segment used to carry — undo, →RS, delete — are gone:
Ctrl+Z undoes the last stroke, *Copy to ▶ an RT structure set* is what →RS
did, and **🗑** deletes whatever is ticked.

**Right-clicking a structure or segment** offers the same set for one row or
the ticked group:

* *Copy … to ▶* / *Move … to ▶* — a submenu of every structure set and
segmentation series in **both** datasets, plus *➕ a new RT structure
set* / *➕ a new segmentation series* as destinations. Right-clicking a
ticked row acts on all ticked rows at once; right-clicking an unticked
row acts on that row alone.
* *🗑 Remove …* — the same single-or-selected rule.
* *💾 Export … as DICOM SEG…* (segments only) — writes the chosen segments
as a SEG series in its own right: same lattice, same referenced image
series, a fresh SOP Instance UID, and only those segments. Exporting three
organs out of twelve therefore needs no special case in the writer, and
the file reloads as an ordinary segmentation series.
* *✎ Rename …* — always the row you clicked, never the whole selection.

Crossing between the two kinds is a conversion, done on transfer: a
Expand Down Expand Up @@ -198,15 +261,16 @@ study was loaded from are never modified.
![comparison mode](screenshot_comparison.png)

*Two opposite breathing phases of the same 4DCT as datasets A and B, each
with its phase-specific structure set; the linked crosshair pins all six
with its phase-specific structure set; the synced crosshair pins all six
views to the same patient-space point inside the tumor.*

Load a second dataset (menu, tree copy/move, or two directories on the
command line) and the window splits into two rows of three views — dataset
A on top, dataset B below. Each dataset keeps its own structures, dose and
plan panels in the sidebar; window/level and dose display settings are
shared. The crosshair is linked between the datasets through **patient
coordinates** (toggleable via *View ▶ Link crosshairs between datasets*);
shared. The crosshair is synced between the datasets through **patient
coordinates** (the toolbar's **🔗**, or *View ▶ Sync crosshairs between
datasets* — both appear only while the crosshair itself is on);
when a registration is active, the link maps through the recovered
transform instead — see [registration.md](registration.md).

Expand All @@ -221,8 +285,10 @@ the rows.

## Planar images (DX / CR / RTIMAGE)

Digital radiographs and RT images (DRRs, portal/setup images) found in the
study folder are listed in the sidebar and open in floating viewer windows
Digital radiographs and RT images (portal/setup images) found in the study
folder — plus any DRR added from the DRR window with *➕ Add to dataset A/B*
(see [drr.md](drr.md)) — are listed in the sidebar and open in floating
viewer windows
with their own window/level (opens at the DICOM default; auto, manual, or
interactive right-drag exactly like the CT views), correct physical aspect
ratio (imager / image-plane pixel spacing), MONOCHROME1 inversion, and the
Expand Down
2 changes: 1 addition & 1 deletion src/app/body_win.rs
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ mod tests {
fn the_tool_names_itself_like_the_others() {
assert_eq!(BODY_CONTOUR.title(0), "👤 Body contour — dataset A");
assert_eq!(BODY_CONTOUR.menu_entry(1), "👤 Body-contour dataset B…");
assert_eq!(BODY_CONTOUR.short_button(), "👤 Body");
assert_eq!(BODY_CONTOUR.short_button(), "👤 Body");
}

#[test]
Expand Down
Loading