Skip to content

Commit 1bdd8a8

Browse files
committed
fix(ultrasound): tighten review follow-ups
- Correct PhysicalUnitsX/Y unit-code table per DICOM PS3.3 C.8.5.5 (10=cm³, 11=cm³/sec, 12=degrees; previously labelled 10 as degrees) and extend the negative test to codes 11–12. - Warn in the console when neither X nor Y direction is centimetres, parallel to the existing multi-region warning, so a 1mm fallback is no longer silent. - Replace `null` with `undefined` for absent ultrasound regions so consumers handle a single absence state instead of three.
1 parent 389b44d commit 1bdd8a8

7 files changed

Lines changed: 35 additions & 34 deletions

File tree

src/core/streaming/chunk.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export class Chunk {
8787
}
8888

8989
get ultrasoundRegions() {
90-
return this.metaLoader.ultrasoundRegions ?? null;
90+
return this.metaLoader.ultrasoundRegions;
9191
}
9292

9393
get dataBlob() {

src/core/streaming/dicom/__tests__/ultrasoundRegion.spec.ts

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -93,30 +93,22 @@ describe('decodeUltrasoundRegion', () => {
9393
});
9494
});
9595

96-
it('returns a null region with zero count when the sequence is empty', () => {
97-
expect(decodeUltrasoundRegion([])).toEqual({
98-
region: null,
99-
regionCount: 0,
100-
});
96+
it('returns no region with zero count when the sequence is empty', () => {
97+
expect(decodeUltrasoundRegion([])).toEqual({ regionCount: 0 });
10198
});
10299

103-
it('returns a null region with zero count when the data is not a sequence', () => {
104-
expect(decodeUltrasoundRegion(undefined)).toEqual({
105-
region: null,
106-
regionCount: 0,
107-
});
100+
it('returns no region with zero count when the data is not a sequence', () => {
101+
expect(decodeUltrasoundRegion(undefined)).toEqual({ regionCount: 0 });
108102
expect(decodeUltrasoundRegion(new Uint8Array(4))).toEqual({
109-
region: null,
110103
regionCount: 0,
111104
});
112105
});
113106

114-
it('returns a null region but reports the count when a required field is missing', () => {
107+
it('returns no region but reports the count when a required field is missing', () => {
115108
const missingDeltaY = wellFormedItem.filter(
116109
(e) => !(e.group === 0x0018 && e.element === 0x602e)
117110
);
118111
expect(decodeUltrasoundRegion(fakeSequenceData([missingDeltaY]))).toEqual({
119-
region: null,
120112
regionCount: 1,
121113
});
122114
});
@@ -143,8 +135,9 @@ describe('unitToMm', () => {
143135

144136
it('returns null for non-spatial unit codes', () => {
145137
// Per DICOM PS3.3 C.8.5.5.1.15: 0=none, 1=percent, 2=dB, 4=seconds,
146-
// 5=hertz, 6=dB/sec, 7=cm/sec, 8=cm², 9=cm²/sec, 10=degrees.
147-
[0, 1, 2, 4, 5, 6, 7, 8, 9, 10].forEach((code) => {
138+
// 5=hertz, 6=dB/sec, 7=cm/sec, 8=cm², 9=cm²/sec, 10=cm³,
139+
// 11=cm³/sec, 12=degrees.
140+
[0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12].forEach((code) => {
148141
expect(unitToMm(code)).toBeNull();
149142
});
150143
});
@@ -210,7 +203,7 @@ describe('parseUltrasoundRegionFromBlob', () => {
210203
});
211204
});
212205

213-
it('returns null when the blob has no SequenceOfUltrasoundRegions', async () => {
206+
it('returns undefined when the blob has no SequenceOfUltrasoundRegions', async () => {
214207
// Build a blob with only the TransferSyntaxUID + pixel data tag.
215208
const preamble = new Uint8Array(128);
216209
const magic = new TextEncoder().encode('DICM');
@@ -221,6 +214,6 @@ describe('parseUltrasoundRegionFromBlob', () => {
221214
new DataView(pixelDataTag.buffer).setUint16(2, 0x0010, true);
222215
const blob = new Blob([concat([preamble, magic, tsx, pixelDataTag])]);
223216

224-
expect(await parseUltrasoundRegionFromBlob(blob)).toBeNull();
217+
expect(await parseUltrasoundRegionFromBlob(blob)).toBeUndefined();
225218
});
226219
});

src/core/streaming/dicom/dicomFileMetaLoader.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99

1010
export class DicomFileMetaLoader implements MetaLoader {
1111
public tags: Maybe<Array<[string, string]>>;
12-
public ultrasoundRegions: UltrasoundRegions | null = null;
12+
public ultrasoundRegions: UltrasoundRegions | undefined;
1313
private file: File;
1414

1515
constructor(

src/core/streaming/dicom/dicomMetaLoader.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export class DicomMetaLoader implements MetaLoader {
3333
private fetcher: Fetcher;
3434
private readDicomTags: ReadDicomTagsFunction;
3535
private blob: Blob | null;
36-
public ultrasoundRegions: UltrasoundRegions | null = null;
36+
public ultrasoundRegions: UltrasoundRegions | undefined;
3737

3838
constructor(fetcher: Fetcher, readDicomTags: ReadDicomTagsFunction) {
3939
this.fetcher = fetcher;
@@ -57,7 +57,7 @@ export class DicomMetaLoader implements MetaLoader {
5757
let explicitVr = true;
5858
let dicomUpToPixelDataIdx = -1;
5959
let modality: string | undefined;
60-
let ultrasoundRegions: UltrasoundRegions | null = null;
60+
let ultrasoundRegions: UltrasoundRegions | undefined;
6161

6262
const parse = createDicomParser({
6363
stopAtElement(group, element) {

src/core/streaming/dicom/ultrasoundRegion.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import { Tags, tagToGroupElement } from '@/src/core/dicomTags';
77
// DICOM unit codes for PhysicalUnitsXDirection / YDirection.
88
// See DICOM PS3.3 C.8.5.5.1.15. The only spatial spacing code defined for
99
// this field is 3 (cm). Other codes (0=none, 1=percent, 2=dB, 4=seconds,
10-
// 5=hertz, 6=dB/seconds, 7=cm/sec, 8=cm², 9=cm²/sec, A=degrees) are time,
11-
// frequency, velocity, area, or angle, so they are not converted to a VTK
12-
// image spacing.
10+
// 5=hertz, 6=dB/seconds, 7=cm/sec, 8=cm², 9=cm²/sec, A=cm³, B=cm³/sec,
11+
// C=degrees) are time, frequency, velocity, area, volume, or angle, so
12+
// they are not converted to a VTK image spacing.
1313
export const US_UNIT_CENTIMETERS = 3;
1414

1515
// Returns the multiplier that converts a physical-delta value in the given
@@ -31,7 +31,7 @@ export type UltrasoundRegion = {
3131
// Doppler) cannot be fully represented with a single VTK image spacing, so
3232
// we expose the count to let callers warn about partial support.
3333
export type UltrasoundRegions = {
34-
region: UltrasoundRegion | null;
34+
region?: UltrasoundRegion;
3535
regionCount: number;
3636
};
3737

@@ -73,13 +73,13 @@ export function decodeUltrasoundRegion(
7373
sequenceData: DataElement['data']
7474
): UltrasoundRegions {
7575
if (!Array.isArray(sequenceData) || sequenceData.length === 0) {
76-
return { region: null, regionCount: 0 };
76+
return { regionCount: 0 };
7777
}
7878
const [firstItem] = sequenceData;
7979

8080
const findBytes = (target: [number, number]) => {
8181
const el = firstItem.find((inner) => isTag(inner, target));
82-
if (!el || !(el.data instanceof Uint8Array)) return null;
82+
if (!el || !(el.data instanceof Uint8Array)) return undefined;
8383
return el.data;
8484
};
8585

@@ -90,7 +90,7 @@ export function decodeUltrasoundRegion(
9090

9191
const regionCount = sequenceData.length;
9292
if (!deltaXBytes || !deltaYBytes || !unitsXBytes || !unitsYBytes) {
93-
return { region: null, regionCount };
93+
return { regionCount };
9494
}
9595

9696
return {
@@ -110,8 +110,8 @@ export function decodeUltrasoundRegion(
110110
*/
111111
export async function parseUltrasoundRegionFromBlob(
112112
blob: Blob
113-
): Promise<UltrasoundRegions | null> {
114-
let regions: UltrasoundRegions | null = null;
113+
): Promise<UltrasoundRegions | undefined> {
114+
let regions: UltrasoundRegions | undefined;
115115

116116
const parse = createDicomParser({
117117
stopAtElement(group, element) {
@@ -136,7 +136,7 @@ export async function parseUltrasoundRegionFromBlob(
136136
}
137137
} catch (err) {
138138
console.warn('Failed to parse SequenceOfUltrasoundRegions:', err);
139-
return null;
139+
return undefined;
140140
} finally {
141141
reader.releaseLock();
142142
}

src/core/streaming/dicomChunkImage.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ export default class DicomChunkImage
286286
private applyUltrasoundSpacing() {
287287
if (this.getModality() !== 'US') return;
288288

289-
const regions = this.chunks[0]?.ultrasoundRegions ?? null;
289+
const regions = this.chunks[0]?.ultrasoundRegions;
290290
if (!regions?.region) return;
291291

292292
// VTK image data has a single global spacing, so multi-region images
@@ -302,7 +302,15 @@ export default class DicomChunkImage
302302
const { region } = regions;
303303
const xFactor = unitToMm(region.physicalUnitsXDirection);
304304
const yFactor = unitToMm(region.physicalUnitsYDirection);
305-
if (xFactor === null || yFactor === null) return;
305+
// All-or-nothing: if either axis lacks a spatial unit (e.g. one axis is
306+
// cm and the other is seconds, or unitless) the metadata can't be trusted
307+
// as a 2D physical spacing, so leave the default 1mm fallback in place.
308+
if (xFactor === null || yFactor === null) {
309+
console.warn(
310+
`Ultrasound spacing not applied: PhysicalUnitsXDirection=${region.physicalUnitsXDirection}, PhysicalUnitsYDirection=${region.physicalUnitsYDirection}; only code 3 (cm) is converted to mm.`
311+
);
312+
return;
313+
}
306314

307315
const [, , zSpacing] = this.vtkImageData.value.getSpacing();
308316
this.vtkImageData.value.setSpacing([

src/core/streaming/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ interface Loader {
1818
export interface MetaLoader extends Loader {
1919
meta: Maybe<Array<[string, string]>>;
2020
metaBlob: Maybe<Blob>;
21-
ultrasoundRegions?: UltrasoundRegions | null;
21+
ultrasoundRegions?: UltrasoundRegions;
2222
}
2323

2424
/**

0 commit comments

Comments
 (0)