dicom-seg-js 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +23 -12
- package/dist/dicom/port.d.ts +22 -3
- package/dist/dicom/port.js +152 -51
- package/dist/errors.d.ts +15 -5
- package/dist/errors.js +18 -5
- package/dist/index.d.ts +5 -1
- package/dist/index.js +30 -14
- package/dist/types.d.ts +11 -3
- package/package.json +7 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-08-30
|
|
4
|
+
|
|
5
|
+
Additive — `^1.0.0` peer unchanged.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`LABELMAP` (PS3.3 Sup 243) read and write.** `readSeg` accepts a LABELMAP SEG
|
|
10
|
+
(one 8- or 16-bit integer-per-pixel frame per plane, pixel value = `SegmentNumber`,
|
|
11
|
+
`0` = background); `seg.type === "LABELMAP"` and `seg.mask(n)` returns the voxels whose
|
|
12
|
+
label is `n`. `writeSeg({ segmentationType: "LABELMAP", segments: [{ number, mask }] })`
|
|
13
|
+
writes one — 8-bit unless a segment number exceeds 255. LABELMAP is a partition:
|
|
14
|
+
overlapping input masks throw the new `LabelmapOverlapError`. `seg.field()` throws
|
|
15
|
+
`SegmentationTypeMismatchError` on a LABELMAP.
|
|
16
|
+
- **`writeSeg({ frameCoverage: "sparse" })`** — omit a frame whose slice is all-background.
|
|
17
|
+
Default stays `"full"` (one frame per segment × plane, exact `writeSeg` → `readSeg`
|
|
18
|
+
identity). Under `"sparse"` the reader reconstructs the grid only across planes that have
|
|
19
|
+
a frame; an all-empty segment throws `RangeError`.
|
|
20
|
+
- `Segmentation.numberOfFrames` — the stored frame count (`< segments × planes` for a
|
|
21
|
+
sparse SEG; one per plane for LABELMAP).
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- `UnsupportedSegmentationTypeError` is no longer raised for LABELMAP (now supported); it
|
|
26
|
+
is reserved for an unknown/vendor `SegmentationType`. An absent `SegmentationType` is a
|
|
27
|
+
`MalformedSegmentationError`.
|
|
28
|
+
- `mask()` now serves BINARY *and* LABELMAP; `field()`'s type-mismatch message names the
|
|
29
|
+
actual type.
|
|
30
|
+
|
|
31
|
+
## [0.1.1] - 2026-08-30
|
|
32
|
+
|
|
33
|
+
No API change. Peer dependency `rt-geometry-js` bumped `^0.1.2` → `^1.0.0` — the shared
|
|
34
|
+
core was promoted to a stable major (identical code to its 0.1.2, see
|
|
35
|
+
`rt-geometry-js/CONTRACT.md`).
|
|
36
|
+
|
|
3
37
|
## [0.1.0] - 2026-08-29
|
|
4
38
|
|
|
5
39
|
First release. DICOM Segmentation (SEG) **read and write** — `BINARY` masks and
|
package/README.md
CHANGED
|
@@ -4,9 +4,11 @@ DICOM **Segmentation (SEG)** reading and writing for JavaScript/TypeScript, buil
|
|
|
4
4
|
[`rt-geometry-js`](https://www.npmjs.com/package/rt-geometry-js). Part of the
|
|
5
5
|
[DICOM imaging toolkit](https://github.com/adeelbarki/dicom-imaging-toolkit-packages).
|
|
6
6
|
|
|
7
|
-
**Status:** 0.
|
|
8
|
-
probability/occupancy fields
|
|
9
|
-
|
|
7
|
+
**Status:** published — [`dicom-seg-js`](https://www.npmjs.com/package/dicom-seg-js) 0.2.0 on npm. Reads **and writes** `BINARY` masks, `FRACTIONAL`
|
|
8
|
+
probability/occupancy fields, and **`LABELMAP`** (PS3.3 Sup 243) — one label per pixel,
|
|
9
|
+
`seg.mask(n)` returns the voxels whose label is `n`. `writeSeg` also takes
|
|
10
|
+
`frameCoverage: "sparse"` to omit all-background frames. Requires the peer
|
|
11
|
+
dependency [`rt-geometry-js`](https://www.npmjs.com/package/rt-geometry-js) (`^1.0.0`);
|
|
10
12
|
`npm install dicom-seg-js rt-geometry-js`.
|
|
11
13
|
|
|
12
14
|
**Validated against real DICOM files**, not just its own fixtures — `dicom-seg-js`'s
|
|
@@ -87,32 +89,41 @@ const probBytes = writeSeg({
|
|
|
87
89
|
```
|
|
88
90
|
|
|
89
91
|
`writeSeg` derives the SEG grid from the first segment's mask/field; every other segment
|
|
90
|
-
must be on the same grid (`GridMismatchError` otherwise).
|
|
91
|
-
`(segment, plane)` across the
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
must be on the same grid (`GridMismatchError` otherwise). By default (`frameCoverage:
|
|
93
|
+
"full"`) one frame is written per `(segment, plane)` across the whole grid, so `writeSeg` →
|
|
94
|
+
`readSeg` is an exact round trip. `frameCoverage: "sparse"` omits a frame whose slice is
|
|
95
|
+
all-background — smaller output, matching how real files are stored, but the reader can
|
|
96
|
+
only reconstruct the grid across planes that have a frame (empty-plane extent is not
|
|
97
|
+
preserved). Pass `fieldScale: "raw"` when your `field` values are already integers in
|
|
94
98
|
`[0, maximumFractionalValue]` rather than `[0, 1]`.
|
|
95
99
|
|
|
100
|
+
**LABELMAP** (`segmentationType: "LABELMAP"`) writes one integer-per-pixel frame per plane,
|
|
101
|
+
each pixel a `SegmentNumber` (`0` = background); 8-bit unless a segment number exceeds 255,
|
|
102
|
+
then 16-bit. It is a **partition** — overlapping input masks raise `LabelmapOverlapError`.
|
|
103
|
+
LABELMAP is multi-*segment*, a different axis from FRACTIONAL's multi-*confidence*; there
|
|
104
|
+
is no `seg.field()` for it.
|
|
105
|
+
|
|
96
106
|
## What it reads
|
|
97
107
|
|
|
98
108
|
| Element | Handling |
|
|
99
109
|
|---|---|
|
|
100
|
-
| `SegmentationType` (0062,0001) | `BINARY` → `mask(n)`; `FRACTIONAL` → `field(n)` / `rawField(n)
|
|
110
|
+
| `SegmentationType` (0062,0001) | `BINARY` → `mask(n)`; `FRACTIONAL` → `field(n)` / `rawField(n)`; `LABELMAP` → `mask(n)` (voxels whose label is `n`). An unknown value → `UnsupportedSegmentationTypeError` |
|
|
101
111
|
| `SegmentSequence` (0062,0002) | number, label, algorithm type/name, coded `SegmentedPropertyCategory` / `Type` / `TypeModifier`, `TrackingID` / `TrackingUID` |
|
|
102
112
|
| `SegmentationFractionalType` (0062,0010) | surfaced on `seg.fractionalType`; **never defaulted** — absent is `undefined` + a diagnostic. A field whose non-zero values are ≥ 98% at the max raises `FRACTIONAL_VALUES_LOOK_BINARY` (a binary mask stored as FRACTIONAL) |
|
|
103
113
|
| `MaximumFractionalValue` (0062,000E) | used to rescale `field(n)` to 0..1; `rawField(n)` keeps the integers. Absent → assumed 255 + a diagnostic |
|
|
104
114
|
| `SegmentsOverlap` (0062,0013) | surfaced on `seg.segmentsOverlap`; `YES` also raises a diagnostic |
|
|
105
115
|
| Per-Frame / Shared Functional Groups | plane positions, orientation, pixel spacing → one `GridGeometry` spanning every frame position |
|
|
106
|
-
| `PixelData` | BINARY bit-unpacked (continuous bitstream; the byte-aligned-per-frame variant is detected and handled with a diagnostic). FRACTIONAL 8-bit |
|
|
116
|
+
| `PixelData` | BINARY bit-unpacked (continuous bitstream; the byte-aligned-per-frame variant is detected and handled with a diagnostic). FRACTIONAL 8-bit. LABELMAP 8- or 16-bit little-endian integers |
|
|
107
117
|
|
|
108
118
|
The SEG's grid is **not** required to match any source image series. To compare a segment
|
|
109
119
|
against a CT or a dose grid, resample it — `resampleMask` / `resampleField` from
|
|
110
120
|
`rt-geometry-js` are re-exported here.
|
|
111
121
|
|
|
112
122
|
Parsing raises `NotSegmentationError` for a non-SEG SOP class,
|
|
113
|
-
`UnsupportedSegmentationTypeError` for
|
|
114
|
-
object can't be assembled (missing `SegmentSequence`
|
|
115
|
-
count ≠ `NumberOfFrames`, a frame referencing an
|
|
123
|
+
`UnsupportedSegmentationTypeError` for an unknown `SegmentationType`, and
|
|
124
|
+
`MalformedSegmentationError` when the object can't be assembled (missing `SegmentSequence`
|
|
125
|
+
/ shared functional groups, per-frame count ≠ `NumberOfFrames`, a frame referencing an
|
|
126
|
+
undeclared segment, short `PixelData`, LABELMAP `BitsAllocated` not 8/16).
|
|
116
127
|
|
|
117
128
|
## License
|
|
118
129
|
|
package/dist/dicom/port.d.ts
CHANGED
|
@@ -19,11 +19,14 @@ export interface ParsedSeg {
|
|
|
19
19
|
readonly numberOfFrames: number;
|
|
20
20
|
readonly segments: readonly SegmentInfo[];
|
|
21
21
|
readonly frames: readonly FrameRef[];
|
|
22
|
-
/** Raw PixelData bytes (bit-packed for BINARY, one byte per pixel for FRACTIONAL
|
|
22
|
+
/** Raw PixelData bytes (bit-packed for BINARY, one byte per pixel for FRACTIONAL,
|
|
23
|
+
* 8- or 16-bit integers per pixel for LABELMAP). */
|
|
23
24
|
readonly pixelData: Uint8Array;
|
|
24
25
|
/** True when BINARY frames are individually padded to a byte boundary rather than
|
|
25
26
|
* packed as one continuous bitstream (a non-conformant but real variant). */
|
|
26
27
|
readonly binaryFramesByteAligned: boolean;
|
|
28
|
+
/** LABELMAP only — bits per label pixel (8 or 16). */
|
|
29
|
+
readonly labelmapBitsAllocated: 8 | 16 | undefined;
|
|
27
30
|
readonly diagnostics: readonly Diagnostic[];
|
|
28
31
|
}
|
|
29
32
|
/** Parse one SEG object's bytes. Throws NotSegmentationError / MalformedSegmentationError /
|
|
@@ -33,6 +36,9 @@ export declare function readSegDataset(bytes: ArrayBuffer): ParsedSeg;
|
|
|
33
36
|
export declare function binaryFrame(parsed: ParsedSeg, frameIndex: number): Uint8Array;
|
|
34
37
|
/** Raw 8-bit values for FRACTIONAL frame `frameIndex` (length rows·columns). */
|
|
35
38
|
export declare function fractionalFrame(parsed: ParsedSeg, frameIndex: number): Uint8Array;
|
|
39
|
+
/** Label values for LABELMAP frame `frameIndex` (length rows·columns; each entry a
|
|
40
|
+
* SegmentNumber, 0 = background). Handles 8- and 16-bit little-endian pixel data. */
|
|
41
|
+
export declare function labelmapFrame(parsed: ParsedSeg, frameIndex: number): Uint8Array | Uint16Array;
|
|
36
42
|
export interface EncodeSegSegment {
|
|
37
43
|
readonly number: number;
|
|
38
44
|
readonly label?: string;
|
|
@@ -45,9 +51,12 @@ export interface EncodeSegSegment {
|
|
|
45
51
|
readonly trackingUid?: string;
|
|
46
52
|
}
|
|
47
53
|
export interface WriteSegFrame {
|
|
54
|
+
/** BINARY / FRACTIONAL: the segment this frame belongs to. LABELMAP: unused (0) — one
|
|
55
|
+
* frame per plane carries every label. */
|
|
48
56
|
readonly segmentNumber: number;
|
|
49
57
|
readonly position: Vec3;
|
|
50
|
-
/** BINARY: 0/1 per pixel. FRACTIONAL: 0..maximumFractionalValue per pixel.
|
|
58
|
+
/** BINARY: 0/1 per pixel. FRACTIONAL: 0..maximumFractionalValue per pixel. LABELMAP: a
|
|
59
|
+
* SegmentNumber (0 = background) per pixel. Length rows·columns. */
|
|
51
60
|
readonly pixels: ArrayLike<number>;
|
|
52
61
|
}
|
|
53
62
|
export interface EncodeSegOptions {
|
|
@@ -62,6 +71,8 @@ export interface EncodeSegOptions {
|
|
|
62
71
|
readonly segmentsOverlap?: SegmentsOverlap;
|
|
63
72
|
readonly fractionalType?: FractionalType;
|
|
64
73
|
readonly maximumFractionalValue?: number;
|
|
74
|
+
/** LABELMAP only — bits per label pixel. Default 8. */
|
|
75
|
+
readonly labelmapBitsAllocated?: 8 | 16;
|
|
65
76
|
readonly segments: readonly EncodeSegSegment[];
|
|
66
77
|
readonly frames: readonly WriteSegFrame[];
|
|
67
78
|
/** Override to exercise NotSegmentationError. */
|
|
@@ -107,8 +118,16 @@ export interface WriteSegOptions {
|
|
|
107
118
|
*/
|
|
108
119
|
readonly fieldScale?: "unit" | "raw";
|
|
109
120
|
readonly segments: readonly WriteSegSegment[];
|
|
110
|
-
/** Default `"NO"` for one segment, `"UNDEFINED"` for more.
|
|
121
|
+
/** Default `"NO"` for one segment, `"UNDEFINED"` for more. Forced to `"NO"` for LABELMAP
|
|
122
|
+
* (a partition). */
|
|
111
123
|
readonly segmentsOverlap?: SegmentsOverlap;
|
|
124
|
+
/**
|
|
125
|
+
* `"full"` (default) — one frame per (segment, plane) over the whole grid, so
|
|
126
|
+
* `writeSeg` → `readSeg` is an exact identity, empty planes included. `"sparse"` — omit a
|
|
127
|
+
* frame whose slice is entirely background (BINARY/FRACTIONAL: all-zero; LABELMAP:
|
|
128
|
+
* all-zero after labelling). Matches how real files are stored; `readSeg` reads either.
|
|
129
|
+
*/
|
|
130
|
+
readonly frameCoverage?: "full" | "sparse";
|
|
112
131
|
/** Default: the shared geometry's `frameOfReferenceUID`, else a fresh UID. */
|
|
113
132
|
readonly frameOfReferenceUID?: string;
|
|
114
133
|
readonly contentLabel?: string;
|
package/dist/dicom/port.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { createDiagnostic, createGridGeometry, cross, DEFAULT_TOLERANCE, dot, normalize, sub, GridMismatchError, } from "rt-geometry-js";
|
|
3
|
-
import { MalformedSegmentationError, NotSegmentationError, UnsupportedSegmentationTypeError, } from "../errors.js";
|
|
3
|
+
import { LabelmapOverlapError, MalformedSegmentationError, NotSegmentationError, UnsupportedSegmentationTypeError, } from "../errors.js";
|
|
4
4
|
// THE ONLY dcmjs importer in dicom-seg-js — mirrors rtstruct-js / rtdose-js port.ts.
|
|
5
5
|
// dcmjs's ESM build is CJS-with-export-syntax and trips Node's resolver; require() hits
|
|
6
6
|
// the genuinely-CJS condition instead.
|
|
@@ -58,13 +58,14 @@ export function readSegDataset(bytes) {
|
|
|
58
58
|
throw new NotSegmentationError(`SOPClassUID ${sopClassUID} is not Segmentation Storage (${SEG_STORAGE_SOP_CLASS_UID}) and Modality is ${modality ?? "absent"}`);
|
|
59
59
|
}
|
|
60
60
|
const rawType = (ds["SegmentationType"] ?? "").toUpperCase();
|
|
61
|
-
if (rawType
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
throw new
|
|
61
|
+
if (rawType !== "BINARY" && rawType !== "FRACTIONAL" && rawType !== "LABELMAP") {
|
|
62
|
+
if (rawType === "") {
|
|
63
|
+
throw new MalformedSegmentationError("SegmentationType (0062,0001) is absent, expected BINARY, FRACTIONAL, or LABELMAP");
|
|
64
|
+
}
|
|
65
|
+
throw new UnsupportedSegmentationTypeError(`SegmentationType is ${JSON.stringify(rawType)} — dicom-seg-js handles BINARY, FRACTIONAL, and LABELMAP`);
|
|
66
66
|
}
|
|
67
67
|
const segmentationType = rawType;
|
|
68
|
+
const isLabelmap = segmentationType === "LABELMAP";
|
|
68
69
|
const rows = ds["Rows"];
|
|
69
70
|
const columns = ds["Columns"];
|
|
70
71
|
const numberOfFrames = ds["NumberOfFrames"] ?? 0;
|
|
@@ -96,9 +97,12 @@ export function readSegDataset(bytes) {
|
|
|
96
97
|
}
|
|
97
98
|
const rawFrames = perFrame.map((fg, frameIndex) => {
|
|
98
99
|
const segId = asArray(fg["SegmentIdentificationSequence"])[0];
|
|
99
|
-
const
|
|
100
|
+
const rawSegNum = Number(segId?.["ReferencedSegmentNumber"]);
|
|
101
|
+
// LABELMAP frames carry every segment via the pixel value, so a per-frame
|
|
102
|
+
// SegmentIdentificationSequence is optional; 0 stands for "all labels".
|
|
103
|
+
const segmentNumber = isLabelmap ? (Number.isInteger(rawSegNum) && rawSegNum > 0 ? rawSegNum : 0) : rawSegNum;
|
|
100
104
|
const ipp = asNumberArray(asArray(fg["PlanePositionSequence"])[0]?.["ImagePositionPatient"]);
|
|
101
|
-
if (!Number.isInteger(segmentNumber) || segmentNumber <= 0) {
|
|
105
|
+
if (!isLabelmap && (!Number.isInteger(segmentNumber) || segmentNumber <= 0)) {
|
|
102
106
|
throw new MalformedSegmentationError(`frame ${frameIndex}: missing SegmentIdentificationSequence/ReferencedSegmentNumber`);
|
|
103
107
|
}
|
|
104
108
|
if (!ipp || ipp.length !== 3) {
|
|
@@ -134,12 +138,16 @@ export function readSegDataset(bytes) {
|
|
|
134
138
|
propertyTypeModifier: readCode(s["SegmentedPropertyTypeModifierCodeSequence"]),
|
|
135
139
|
trackingId: s["TrackingID"] || undefined,
|
|
136
140
|
trackingUid: s["TrackingUID"] || undefined,
|
|
137
|
-
|
|
141
|
+
// LABELMAP stores one frame per plane carrying every label, so a per-segment count
|
|
142
|
+
// isn't meaningful before the pixels are scanned — report the plane count.
|
|
143
|
+
frameCount: isLabelmap ? perFrame.length : framesBySegment.get(number) ?? 0,
|
|
138
144
|
};
|
|
139
145
|
});
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
146
|
+
if (!isLabelmap) {
|
|
147
|
+
for (const f of rawFrames) {
|
|
148
|
+
if (!declaredNumbers.has(f.segmentNumber)) {
|
|
149
|
+
throw new MalformedSegmentationError(`frame ${f.frameIndex} references SegmentNumber ${f.segmentNumber}, which has no SegmentSequence entry`);
|
|
150
|
+
}
|
|
143
151
|
}
|
|
144
152
|
}
|
|
145
153
|
// --- geometry: distinct plane positions across every frame ---
|
|
@@ -189,7 +197,19 @@ export function readSegDataset(bytes) {
|
|
|
189
197
|
const pixelData = new Uint8Array(first);
|
|
190
198
|
const rc = rows * columns;
|
|
191
199
|
let binaryFramesByteAligned = false;
|
|
192
|
-
|
|
200
|
+
let labelmapBitsAllocated;
|
|
201
|
+
if (isLabelmap) {
|
|
202
|
+
const ba = Number(ds["BitsAllocated"]);
|
|
203
|
+
if (ba !== 8 && ba !== 16) {
|
|
204
|
+
throw new MalformedSegmentationError(`LABELMAP BitsAllocated is ${ds["BitsAllocated"]}, expected 8 or 16`);
|
|
205
|
+
}
|
|
206
|
+
labelmapBitsAllocated = ba;
|
|
207
|
+
const need = numberOfFrames * rc * (ba / 8);
|
|
208
|
+
if (pixelData.length < need) {
|
|
209
|
+
throw new MalformedSegmentationError(`PixelData is ${pixelData.length} bytes; a LABELMAP SEG of ${numberOfFrames} frames × ${rc} px × ${ba / 8} B needs ${need}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else if (segmentationType === "BINARY") {
|
|
193
213
|
const continuousBytes = Math.ceil((numberOfFrames * rc) / 8);
|
|
194
214
|
const perFrameBytes = numberOfFrames * Math.ceil(rc / 8);
|
|
195
215
|
if (pixelData.length >= continuousBytes) {
|
|
@@ -267,6 +287,7 @@ export function readSegDataset(bytes) {
|
|
|
267
287
|
frames,
|
|
268
288
|
pixelData,
|
|
269
289
|
binaryFramesByteAligned,
|
|
290
|
+
labelmapBitsAllocated,
|
|
270
291
|
diagnostics,
|
|
271
292
|
};
|
|
272
293
|
}
|
|
@@ -302,6 +323,23 @@ export function fractionalFrame(parsed, frameIndex) {
|
|
|
302
323
|
const offset = frameIndex * rc;
|
|
303
324
|
return parsed.pixelData.subarray(offset, offset + rc);
|
|
304
325
|
}
|
|
326
|
+
/** Label values for LABELMAP frame `frameIndex` (length rows·columns; each entry a
|
|
327
|
+
* SegmentNumber, 0 = background). Handles 8- and 16-bit little-endian pixel data. */
|
|
328
|
+
export function labelmapFrame(parsed, frameIndex) {
|
|
329
|
+
const rc = parsed.rows * parsed.columns;
|
|
330
|
+
if (parsed.labelmapBitsAllocated === 16) {
|
|
331
|
+
const byteOffset = parsed.pixelData.byteOffset + frameIndex * rc * 2;
|
|
332
|
+
// A copy, so callers can't accidentally mutate the shared PixelData, and to sidestep
|
|
333
|
+
// any alignment constraint on the underlying buffer.
|
|
334
|
+
const out = new Uint16Array(rc);
|
|
335
|
+
const view = new DataView(parsed.pixelData.buffer, byteOffset, rc * 2);
|
|
336
|
+
for (let i = 0; i < rc; i++)
|
|
337
|
+
out[i] = view.getUint16(i * 2, true);
|
|
338
|
+
return out;
|
|
339
|
+
}
|
|
340
|
+
const offset = frameIndex * rc;
|
|
341
|
+
return parsed.pixelData.subarray(offset, offset + rc);
|
|
342
|
+
}
|
|
305
343
|
export function encodeSegFrames(options) {
|
|
306
344
|
const rows = options.rows;
|
|
307
345
|
const columns = options.columns;
|
|
@@ -309,10 +347,13 @@ export function encodeSegFrames(options) {
|
|
|
309
347
|
const rowDirection = options.rowDirection ?? [1, 0, 0];
|
|
310
348
|
const columnDirection = options.columnDirection ?? [0, 1, 0];
|
|
311
349
|
const isBinary = options.segmentationType === "BINARY";
|
|
350
|
+
const isLabelmap = options.segmentationType === "LABELMAP";
|
|
351
|
+
const labelmapBits = options.labelmapBitsAllocated ?? 8;
|
|
312
352
|
const perFrameGroups = options.frames.map((f) => ({
|
|
313
|
-
FrameContentSequence: [{ StackID: "1", InStackPositionNumber: 1, DimensionIndexValues: [f.segmentNumber, 1] }],
|
|
353
|
+
FrameContentSequence: [{ StackID: "1", InStackPositionNumber: 1, DimensionIndexValues: [f.segmentNumber || 1, 1] }],
|
|
314
354
|
PlanePositionSequence: [{ ImagePositionPatient: [...f.position] }],
|
|
315
|
-
|
|
355
|
+
// LABELMAP carries the segment in the pixel value — no per-frame SegmentIdentificationSequence.
|
|
356
|
+
...(isLabelmap ? {} : { SegmentIdentificationSequence: [{ ReferencedSegmentNumber: f.segmentNumber }] }),
|
|
316
357
|
}));
|
|
317
358
|
let pixelBuffer;
|
|
318
359
|
if (isBinary) {
|
|
@@ -322,6 +363,25 @@ export function encodeSegFrames(options) {
|
|
|
322
363
|
bits.push(f.pixels[i] ? 1 : 0);
|
|
323
364
|
pixelBuffer = BitArray.pack(bits).buffer.slice(0);
|
|
324
365
|
}
|
|
366
|
+
else if (isLabelmap) {
|
|
367
|
+
const n = options.frames.length * rc;
|
|
368
|
+
if (labelmapBits === 16) {
|
|
369
|
+
const words = new Uint16Array(n);
|
|
370
|
+
options.frames.forEach((f, fi) => {
|
|
371
|
+
for (let i = 0; i < rc; i++)
|
|
372
|
+
words[fi * rc + i] = Math.max(0, Math.round(f.pixels[i] ?? 0)) & 0xffff;
|
|
373
|
+
});
|
|
374
|
+
pixelBuffer = words.buffer.slice(0);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
const bytes = new Uint8Array(n);
|
|
378
|
+
options.frames.forEach((f, fi) => {
|
|
379
|
+
for (let i = 0; i < rc; i++)
|
|
380
|
+
bytes[fi * rc + i] = Math.max(0, Math.min(255, Math.round(f.pixels[i] ?? 0)));
|
|
381
|
+
});
|
|
382
|
+
pixelBuffer = bytes.buffer.slice(0);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
325
385
|
else {
|
|
326
386
|
const bytes = new Uint8Array(options.frames.length * rc);
|
|
327
387
|
options.frames.forEach((f, fi) => {
|
|
@@ -340,9 +400,9 @@ export function encodeSegFrames(options) {
|
|
|
340
400
|
Rows: rows,
|
|
341
401
|
Columns: columns,
|
|
342
402
|
NumberOfFrames: options.frames.length,
|
|
343
|
-
BitsAllocated: isBinary ? 1 : 8,
|
|
344
|
-
BitsStored: isBinary ? 1 : 8,
|
|
345
|
-
HighBit: isBinary ? 0 : 7,
|
|
403
|
+
BitsAllocated: isBinary ? 1 : isLabelmap ? labelmapBits : 8,
|
|
404
|
+
BitsStored: isBinary ? 1 : isLabelmap ? labelmapBits : 8,
|
|
405
|
+
HighBit: isBinary ? 0 : isLabelmap ? labelmapBits - 1 : 7,
|
|
346
406
|
PixelRepresentation: 0,
|
|
347
407
|
SegmentationType: options.forceType ?? options.segmentationType,
|
|
348
408
|
SegmentsOverlap: options.segmentsOverlap ?? "NO",
|
|
@@ -388,7 +448,7 @@ export function encodeSegFrames(options) {
|
|
|
388
448
|
],
|
|
389
449
|
PerFrameFunctionalGroupsSequence: perFrameGroups,
|
|
390
450
|
};
|
|
391
|
-
if (!isBinary) {
|
|
451
|
+
if (!isBinary && !isLabelmap) {
|
|
392
452
|
if (!options.omitFractionalType)
|
|
393
453
|
dataset["SegmentationFractionalType"] = options.fractionalType ?? "PROBABILITY";
|
|
394
454
|
if (!options.omitMaximumFractionalValue)
|
|
@@ -403,20 +463,23 @@ export function encodeSegFrames(options) {
|
|
|
403
463
|
};
|
|
404
464
|
const dicomDict = new DicomDict(DicomMetaDictionary.denaturalizeDataset(meta));
|
|
405
465
|
dicomDict.dict = DicomMetaDictionary.denaturalizeDataset(dataset);
|
|
406
|
-
dicomDict.dict[TAG_PIXEL_DATA] = { vr: "OB", Value: [pixelBuffer] };
|
|
466
|
+
dicomDict.dict[TAG_PIXEL_DATA] = { vr: isLabelmap && labelmapBits === 16 ? "OW" : "OB", Value: [pixelBuffer] };
|
|
407
467
|
return dicomDict.write();
|
|
408
468
|
}
|
|
409
469
|
export function writeSeg(options) {
|
|
410
470
|
const isBinary = options.segmentationType === "BINARY";
|
|
411
|
-
|
|
412
|
-
|
|
471
|
+
const isLabelmap = options.segmentationType === "LABELMAP";
|
|
472
|
+
const isFractional = options.segmentationType === "FRACTIONAL";
|
|
473
|
+
if (!isBinary && !isFractional && !isLabelmap) {
|
|
474
|
+
throw new TypeError(`writeSeg: segmentationType must be "BINARY", "FRACTIONAL", or "LABELMAP", got ${JSON.stringify(options.segmentationType)}`);
|
|
413
475
|
}
|
|
414
|
-
if (
|
|
476
|
+
if (isFractional && options.fractionalType === undefined) {
|
|
415
477
|
throw new TypeError('writeSeg: a FRACTIONAL segmentation requires an explicit fractionalType ("PROBABILITY" or "OCCUPANCY") — ' +
|
|
416
478
|
"there is no default (FRACTIONAL-SEG.md §1)");
|
|
417
479
|
}
|
|
418
480
|
if (options.segments.length === 0)
|
|
419
481
|
throw new RangeError("writeSeg: at least one segment is required");
|
|
482
|
+
const sparse = options.frameCoverage === "sparse";
|
|
420
483
|
const numbers = new Set();
|
|
421
484
|
for (const s of options.segments) {
|
|
422
485
|
if (!Number.isInteger(s.number) || s.number <= 0) {
|
|
@@ -427,21 +490,22 @@ export function writeSeg(options) {
|
|
|
427
490
|
numbers.add(s.number);
|
|
428
491
|
}
|
|
429
492
|
const maxFractional = options.maximumFractionalValue ?? 255;
|
|
430
|
-
if (
|
|
493
|
+
if (isFractional && (!Number.isInteger(maxFractional) || maxFractional < 1 || maxFractional > 255)) {
|
|
431
494
|
throw new RangeError(`writeSeg: maximumFractionalValue must be an integer in [1, 255], got ${maxFractional}`);
|
|
432
495
|
}
|
|
433
496
|
const rawScale = options.fieldScale === "raw";
|
|
497
|
+
const wantsMask = isBinary || isLabelmap;
|
|
434
498
|
const structures = options.segments.map((s) => {
|
|
435
|
-
const structure =
|
|
499
|
+
const structure = wantsMask ? s.mask : s.field;
|
|
436
500
|
if (!structure) {
|
|
437
|
-
throw new TypeError(`writeSeg: segment ${s.number} is missing its ${
|
|
501
|
+
throw new TypeError(`writeSeg: segment ${s.number} is missing its ${wantsMask ? "mask" : "field"}`);
|
|
438
502
|
}
|
|
439
503
|
return structure;
|
|
440
504
|
});
|
|
441
505
|
const geometry = structures[0].geometry;
|
|
442
506
|
for (let i = 1; i < structures.length; i++) {
|
|
443
507
|
if (!structures[i].geometry.equals(geometry, options.tolerance)) {
|
|
444
|
-
throw new GridMismatchError(`writeSeg: segment ${options.segments[i].number}'s ${
|
|
508
|
+
throw new GridMismatchError(`writeSeg: segment ${options.segments[i].number}'s ${wantsMask ? "mask" : "field"} is on a different grid than segment ${options.segments[0].number}'s — every segment must share one GridGeometry`);
|
|
445
509
|
}
|
|
446
510
|
}
|
|
447
511
|
const columns = geometry.columns;
|
|
@@ -450,34 +514,70 @@ export function writeSeg(options) {
|
|
|
450
514
|
const planeCount = geometry.planes.length;
|
|
451
515
|
const planePositions = geometry.planes.map((p) => p.position);
|
|
452
516
|
const sliceThickness = planeCount >= 2 ? geometry.planeThicknessMm(0) : 1;
|
|
453
|
-
//
|
|
454
|
-
// writeSeg → readSeg is an exact identity
|
|
455
|
-
// (Real files usually omit all-zero frames; reading handles that, sparse
|
|
456
|
-
// *writing* is a later feature.)
|
|
517
|
+
// `"full"` (default): one frame per (segment, plane) over the whole grid, so
|
|
518
|
+
// writeSeg → readSeg is an exact identity. `"sparse"`: skip an all-background slice.
|
|
457
519
|
const frames = [];
|
|
458
|
-
|
|
459
|
-
|
|
520
|
+
let labelmapBits = 8;
|
|
521
|
+
if (isLabelmap) {
|
|
522
|
+
const maxSeg = Math.max(...options.segments.map((s) => s.number));
|
|
523
|
+
labelmapBits = maxSeg > 255 ? 16 : 8;
|
|
460
524
|
for (let k = 0; k < planeCount; k++) {
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
for (let
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
525
|
+
const pixels = maxSeg > 255 ? new Uint16Array(rc) : new Uint8Array(rc);
|
|
526
|
+
let anySet = false;
|
|
527
|
+
for (let si = 0; si < options.segments.length; si++) {
|
|
528
|
+
const s = options.segments[si];
|
|
529
|
+
const slice = structures[si].getSliceBuffer(k);
|
|
530
|
+
for (let i = 0; i < rc; i++) {
|
|
531
|
+
if (slice[i] === 0)
|
|
532
|
+
continue;
|
|
533
|
+
if (pixels[i] !== 0) {
|
|
534
|
+
throw new LabelmapOverlapError(`segments ${pixels[i]} and ${s.number} both set the voxel at plane ${k}, index ${i} — ` +
|
|
535
|
+
"LABELMAP stores one label per pixel. Use segmentationType \"BINARY\" for overlapping segments.");
|
|
536
|
+
}
|
|
537
|
+
pixels[i] = s.number;
|
|
538
|
+
anySet = true;
|
|
475
539
|
}
|
|
476
540
|
}
|
|
477
|
-
|
|
541
|
+
if (sparse && !anySet)
|
|
542
|
+
continue;
|
|
543
|
+
frames.push({ segmentNumber: 0, position: planePositions[k], pixels });
|
|
478
544
|
}
|
|
479
|
-
}
|
|
480
|
-
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
options.segments.forEach((s, si) => {
|
|
548
|
+
const structure = structures[si];
|
|
549
|
+
for (let k = 0; k < planeCount; k++) {
|
|
550
|
+
const slice = structure.getSliceBuffer(k);
|
|
551
|
+
const pixels = new Uint8Array(rc);
|
|
552
|
+
let anySet = false;
|
|
553
|
+
for (let i = 0; i < rc; i++) {
|
|
554
|
+
const v = slice[i];
|
|
555
|
+
if (isBinary) {
|
|
556
|
+
pixels[i] = v !== 0 ? 1 : 0;
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
let stored = Math.round(rawScale ? v : v * maxFractional);
|
|
560
|
+
if (stored < 0)
|
|
561
|
+
stored = 0;
|
|
562
|
+
if (stored > maxFractional)
|
|
563
|
+
stored = maxFractional;
|
|
564
|
+
pixels[i] = stored;
|
|
565
|
+
}
|
|
566
|
+
if (pixels[i] !== 0)
|
|
567
|
+
anySet = true;
|
|
568
|
+
}
|
|
569
|
+
if (sparse && !anySet)
|
|
570
|
+
continue;
|
|
571
|
+
frames.push({ segmentNumber: s.number, position: planePositions[k], pixels });
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
if (frames.length === 0) {
|
|
576
|
+
throw new RangeError("writeSeg: frameCoverage \"sparse\" produced no frames — every segment is empty");
|
|
577
|
+
}
|
|
578
|
+
const segmentsOverlap = isLabelmap
|
|
579
|
+
? "NO"
|
|
580
|
+
: options.segmentsOverlap ?? (options.segments.length === 1 ? "NO" : "UNDEFINED");
|
|
481
581
|
const encodeOptions = {
|
|
482
582
|
rows,
|
|
483
583
|
columns,
|
|
@@ -502,7 +602,8 @@ export function writeSeg(options) {
|
|
|
502
602
|
...(options.frameOfReferenceUID ?? geometry.frameOfReferenceUID
|
|
503
603
|
? { frameOfReferenceUID: (options.frameOfReferenceUID ?? geometry.frameOfReferenceUID) }
|
|
504
604
|
: {}),
|
|
505
|
-
...(
|
|
605
|
+
...(isFractional ? { fractionalType: options.fractionalType, maximumFractionalValue: maxFractional } : {}),
|
|
606
|
+
...(isLabelmap ? { labelmapBitsAllocated: labelmapBits } : {}),
|
|
506
607
|
};
|
|
507
608
|
return encodeSegFrames(encodeOptions);
|
|
508
609
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -22,16 +22,26 @@ export declare class MalformedSegmentationError extends Error {
|
|
|
22
22
|
constructor(message: string);
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
* `SegmentationType` (0062,0001) is
|
|
26
|
-
* `FRACTIONAL
|
|
25
|
+
* `SegmentationType` (0062,0001) is a value dicom-seg-js does not handle. As of 0.2.0
|
|
26
|
+
* `BINARY`, `FRACTIONAL`, and `LABELMAP` (PS3.3 Supplement 243) are all supported, so this
|
|
27
|
+
* is now raised only for an unknown/vendor value.
|
|
27
28
|
*/
|
|
28
29
|
export declare class UnsupportedSegmentationTypeError extends Error {
|
|
29
30
|
constructor(message: string);
|
|
30
31
|
}
|
|
31
32
|
/**
|
|
32
|
-
* `
|
|
33
|
-
*
|
|
34
|
-
*
|
|
33
|
+
* A `writeSeg({ segmentationType: "LABELMAP" })` call was given segments whose masks
|
|
34
|
+
* overlap. LABELMAP is a partition — each pixel stores exactly one `SegmentNumber` — so
|
|
35
|
+
* overlapping input cannot be represented. Use `BINARY` (with `SegmentsOverlap`) instead.
|
|
36
|
+
*/
|
|
37
|
+
export declare class LabelmapOverlapError extends Error {
|
|
38
|
+
constructor(message: string);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* `mask()` was called on a FRACTIONAL segmentation, `field()` on a BINARY or LABELMAP one,
|
|
42
|
+
* or `field()` on LABELMAP. The forms are not interchangeable and there is no safe default
|
|
43
|
+
* threshold to turn a probability field into a mask (roadmap §7.1) — the caller must pick
|
|
44
|
+
* one explicitly.
|
|
35
45
|
*/
|
|
36
46
|
export declare class SegmentationTypeMismatchError extends Error {
|
|
37
47
|
constructor(message: string);
|
package/dist/errors.js
CHANGED
|
@@ -28,8 +28,9 @@ export class MalformedSegmentationError extends Error {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* `SegmentationType` (0062,0001) is
|
|
32
|
-
* `FRACTIONAL
|
|
31
|
+
* `SegmentationType` (0062,0001) is a value dicom-seg-js does not handle. As of 0.2.0
|
|
32
|
+
* `BINARY`, `FRACTIONAL`, and `LABELMAP` (PS3.3 Supplement 243) are all supported, so this
|
|
33
|
+
* is now raised only for an unknown/vendor value.
|
|
33
34
|
*/
|
|
34
35
|
export class UnsupportedSegmentationTypeError extends Error {
|
|
35
36
|
constructor(message) {
|
|
@@ -38,9 +39,21 @@ export class UnsupportedSegmentationTypeError extends Error {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
/**
|
|
41
|
-
* `
|
|
42
|
-
*
|
|
43
|
-
*
|
|
42
|
+
* A `writeSeg({ segmentationType: "LABELMAP" })` call was given segments whose masks
|
|
43
|
+
* overlap. LABELMAP is a partition — each pixel stores exactly one `SegmentNumber` — so
|
|
44
|
+
* overlapping input cannot be represented. Use `BINARY` (with `SegmentsOverlap`) instead.
|
|
45
|
+
*/
|
|
46
|
+
export class LabelmapOverlapError extends Error {
|
|
47
|
+
constructor(message) {
|
|
48
|
+
super(`LabelmapOverlapError: ${message}`);
|
|
49
|
+
this.name = "LabelmapOverlapError";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* `mask()` was called on a FRACTIONAL segmentation, `field()` on a BINARY or LABELMAP one,
|
|
54
|
+
* or `field()` on LABELMAP. The forms are not interchangeable and there is no safe default
|
|
55
|
+
* threshold to turn a probability field into a mask (roadmap §7.1) — the caller must pick
|
|
56
|
+
* one explicitly.
|
|
44
57
|
*/
|
|
45
58
|
export class SegmentationTypeMismatchError extends Error {
|
|
46
59
|
constructor(message) {
|
package/dist/index.d.ts
CHANGED
|
@@ -39,6 +39,9 @@ export declare class Segmentation {
|
|
|
39
39
|
readonly geometry: GridGeometry;
|
|
40
40
|
readonly frameOfReferenceUID: string | undefined;
|
|
41
41
|
readonly contentLabel: string | undefined;
|
|
42
|
+
/** `NumberOfFrames` (0028,0008) — total frames stored. A sparse SEG has fewer than
|
|
43
|
+
* `segments × planes`; a LABELMAP has one per plane. */
|
|
44
|
+
readonly numberOfFrames: number;
|
|
42
45
|
readonly diagnostics: readonly Diagnostic[];
|
|
43
46
|
private readonly parsed;
|
|
44
47
|
private readonly maskCache;
|
|
@@ -52,7 +55,8 @@ export declare class Segmentation {
|
|
|
52
55
|
private assertSegment;
|
|
53
56
|
private get sliceSize();
|
|
54
57
|
/**
|
|
55
|
-
* The boolean mask for a **BINARY** segment.
|
|
58
|
+
* The boolean mask for a segment. **BINARY** — the segment's frames. **LABELMAP** — the
|
|
59
|
+
* voxels whose label equals `segmentNumber`. Throws {@link SegmentationTypeMismatchError}
|
|
56
60
|
* on a FRACTIONAL SEG — threshold `field(n)` yourself, there is no safe default cut.
|
|
57
61
|
*/
|
|
58
62
|
mask(segmentNumber: number): Mask3D;
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* segment to a resample or a histogram from one import.
|
|
15
15
|
*/
|
|
16
16
|
import { createScalarField, maskFromDense, sampleFieldAt, } from "rt-geometry-js";
|
|
17
|
-
import { binaryFrame, fractionalFrame, readSegDataset } from "./dicom/port.js";
|
|
17
|
+
import { binaryFrame, fractionalFrame, labelmapFrame, readSegDataset } from "./dicom/port.js";
|
|
18
18
|
import { SegmentationTypeMismatchError } from "./errors.js";
|
|
19
19
|
export * from "rt-geometry-js";
|
|
20
20
|
export * from "./types.js";
|
|
@@ -41,6 +41,9 @@ export class Segmentation {
|
|
|
41
41
|
geometry;
|
|
42
42
|
frameOfReferenceUID;
|
|
43
43
|
contentLabel;
|
|
44
|
+
/** `NumberOfFrames` (0028,0008) — total frames stored. A sparse SEG has fewer than
|
|
45
|
+
* `segments × planes`; a LABELMAP has one per plane. */
|
|
46
|
+
numberOfFrames;
|
|
44
47
|
diagnostics;
|
|
45
48
|
parsed;
|
|
46
49
|
maskCache = new Map();
|
|
@@ -55,6 +58,7 @@ export class Segmentation {
|
|
|
55
58
|
this.geometry = parsed.geometry;
|
|
56
59
|
this.frameOfReferenceUID = parsed.frameOfReferenceUID;
|
|
57
60
|
this.contentLabel = parsed.contentLabel;
|
|
61
|
+
this.numberOfFrames = parsed.numberOfFrames;
|
|
58
62
|
this.diagnostics = parsed.diagnostics;
|
|
59
63
|
}
|
|
60
64
|
static fromDicom(bytes) {
|
|
@@ -76,27 +80,39 @@ export class Segmentation {
|
|
|
76
80
|
return this.parsed.rows * this.parsed.columns;
|
|
77
81
|
}
|
|
78
82
|
/**
|
|
79
|
-
* The boolean mask for a **BINARY** segment.
|
|
83
|
+
* The boolean mask for a segment. **BINARY** — the segment's frames. **LABELMAP** — the
|
|
84
|
+
* voxels whose label equals `segmentNumber`. Throws {@link SegmentationTypeMismatchError}
|
|
80
85
|
* on a FRACTIONAL SEG — threshold `field(n)` yourself, there is no safe default cut.
|
|
81
86
|
*/
|
|
82
87
|
mask(segmentNumber) {
|
|
83
88
|
this.assertSegment(segmentNumber);
|
|
84
|
-
if (this.type
|
|
85
|
-
throw new SegmentationTypeMismatchError(`mask() is for BINARY segmentations; this is FRACTIONAL — use field(${segmentNumber}) and apply your own threshold`);
|
|
89
|
+
if (this.type === "FRACTIONAL") {
|
|
90
|
+
throw new SegmentationTypeMismatchError(`mask() is for BINARY / LABELMAP segmentations; this is FRACTIONAL — use field(${segmentNumber}) and apply your own threshold`);
|
|
86
91
|
}
|
|
87
92
|
const cached = this.maskCache.get(segmentNumber);
|
|
88
93
|
if (cached)
|
|
89
94
|
return cached;
|
|
90
95
|
const rc = this.sliceSize;
|
|
91
96
|
const data = new Uint8Array(this.parsed.geometry.planes.length * rc);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
if (this.type === "LABELMAP") {
|
|
98
|
+
for (const fr of this.parsed.frames) {
|
|
99
|
+
const labels = labelmapFrame(this.parsed, fr.frameIndex);
|
|
100
|
+
const base = fr.planeIndex * rc;
|
|
101
|
+
for (let i = 0; i < rc; i++)
|
|
102
|
+
if (labels[i] === segmentNumber)
|
|
103
|
+
data[base + i] = 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
for (const fr of this.parsed.frames) {
|
|
108
|
+
if (fr.segmentNumber !== segmentNumber)
|
|
109
|
+
continue;
|
|
110
|
+
const bits = binaryFrame(this.parsed, fr.frameIndex);
|
|
111
|
+
const base = fr.planeIndex * rc;
|
|
112
|
+
for (let i = 0; i < rc; i++)
|
|
113
|
+
if (bits[i])
|
|
114
|
+
data[base + i] = 1;
|
|
115
|
+
}
|
|
100
116
|
}
|
|
101
117
|
const mask = maskFromDense(this.parsed.geometry, data);
|
|
102
118
|
this.maskCache.set(segmentNumber, mask);
|
|
@@ -117,7 +133,7 @@ export class Segmentation {
|
|
|
117
133
|
fractionalField(segmentNumber, rescale, cache) {
|
|
118
134
|
this.assertSegment(segmentNumber);
|
|
119
135
|
if (this.type !== "FRACTIONAL") {
|
|
120
|
-
throw new SegmentationTypeMismatchError(`field() is for FRACTIONAL segmentations; this is
|
|
136
|
+
throw new SegmentationTypeMismatchError(`field() is for FRACTIONAL segmentations; this is ${this.type} — use mask(${segmentNumber})`);
|
|
121
137
|
}
|
|
122
138
|
const cached = cache.get(segmentNumber);
|
|
123
139
|
if (cached)
|
|
@@ -144,7 +160,7 @@ export class Segmentation {
|
|
|
144
160
|
* region the model marked at all".
|
|
145
161
|
*/
|
|
146
162
|
support(segmentNumber) {
|
|
147
|
-
if (this.type === "BINARY")
|
|
163
|
+
if (this.type === "BINARY" || this.type === "LABELMAP")
|
|
148
164
|
return this.mask(segmentNumber);
|
|
149
165
|
const raw = this.rawField(segmentNumber);
|
|
150
166
|
const [columns, rows, planes] = raw.dimensions;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* `SegmentationType` (0062,0001).
|
|
3
|
+
*
|
|
4
|
+
* - `BINARY` — 1 bit per pixel, one frame per (segment, plane).
|
|
5
|
+
* - `FRACTIONAL` — 8 bits per pixel, `0..MaximumFractionalValue`, one frame per
|
|
6
|
+
* (segment, plane). {@link FractionalType} says whether the value is a probability or an
|
|
7
|
+
* occupancy.
|
|
8
|
+
* - `LABELMAP` (PS3.3 Sup 243) — one frame per **plane**, each pixel an integer equal to
|
|
9
|
+
* the `SegmentNumber` it belongs to (`0` = background). A partition: a voxel has at most
|
|
10
|
+
* one label. `seg.mask(n)` returns the voxels whose label is `n`; there is no
|
|
11
|
+
* `seg.field()`.
|
|
4
12
|
*/
|
|
5
|
-
export type SegmentationType = "BINARY" | "FRACTIONAL";
|
|
13
|
+
export type SegmentationType = "BINARY" | "FRACTIONAL" | "LABELMAP";
|
|
6
14
|
/**
|
|
7
15
|
* `SegmentationFractionalType` (0062,0010). PROBABILITY = "the probability that the
|
|
8
16
|
* segmented property occupies the voxel"; OCCUPANCY = "the fraction of the voxel volume
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dicom-seg-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "DICOM Segmentation (SEG) reading — BINARY masks and FRACTIONAL probability/occupancy fields — built on rt-geometry-js.",
|
|
5
|
+
"description": "DICOM Segmentation (SEG) reading and writing — BINARY masks and FRACTIONAL probability/occupancy fields — built on rt-geometry-js.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dicom",
|
|
8
8
|
"segmentation",
|
|
@@ -48,16 +48,17 @@
|
|
|
48
48
|
"check:deps": "node ../../scripts/check-dependency-rule.mjs",
|
|
49
49
|
"build": "tsc -p tsconfig.build.json",
|
|
50
50
|
"prebuild": "npm run build --workspace rt-geometry-js",
|
|
51
|
-
"prepublishOnly": "npm run build && npm test"
|
|
51
|
+
"prepublishOnly": "npm run build && npm test",
|
|
52
|
+
"bench": "vitest bench --run"
|
|
52
53
|
},
|
|
53
54
|
"peerDependencies": {
|
|
54
|
-
"rt-geometry-js": "^0.
|
|
55
|
+
"rt-geometry-js": "^1.0.0"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
57
58
|
"@types/node": "^26.2.0",
|
|
58
|
-
"rt-geometry-js": "^0.
|
|
59
|
+
"rt-geometry-js": "^1.0.0",
|
|
59
60
|
"typescript": "^5.6.3",
|
|
60
|
-
"vitest": "^2.
|
|
61
|
+
"vitest": "^3.2.7"
|
|
61
62
|
},
|
|
62
63
|
"dependencies": {
|
|
63
64
|
"dcmjs": "^0.52.0"
|