feat(b-values): implement parsing for multiple b-value formats and update default setting

This commit is contained in:
Ross
2026-05-19 13:26:32 +01:00
parent b0d8c72636
commit 41f13ade5e
2 changed files with 406 additions and 265 deletions
+58
View File
@@ -123,6 +123,64 @@
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_596_stFfHXn.dcm",
],
},
{
i: [
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_792_og2P5S6.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_793_3tKkg87.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_794_s8FB3dq.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_795_8HeRpXV.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_796_tllvzjH.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_797_AXieNgu.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_798_L8YapjK.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_799_kvuLUsW.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_800_i2eSNzF.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_801_dR3KPL5.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_802_cDgm138.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_803_Bj3PZyX.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_804_CEqcWo2.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_805_5R2LmiA.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_806_RgmKU6b.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_807_BKTkmJv.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_808_OQOf2MC.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_809_lxnU3mr.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_810_8vG5Ett.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_811_poX6D2s.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_812_k46Ws88.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_813_7Ft4JOE.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_814_V5lJKxR.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_815_1hFehRl.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_816_34UqY7Z.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_817_YG28gpL.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_818_wonhIz5.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_819_bv3zaOx.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_820_xEjnfcI.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_821_cH9fPxl.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_822_lWmBHxs.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_823_ybix6Gz.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_824_q430lCK.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_825_28trFVc.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_826_7BF6HBF.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_827_ipmO3HH.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_828_Zuppb9h.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_829_TMuror5.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_830_Mnaj7oR.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_831_2U6zI7M.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_832_V9fvVdM.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_833_AAFaoOW.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_834_vxBJLIn.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_835_ZppFa6u.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_836_adRHoD3.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_837_jjfHCn3.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_838_N21h3ne.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_839_n6FWmeX.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_840_dErur0M.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_841_PcoTakk.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_842_UmTu7Fg.dcm",
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/IMG_843_USByQXH.dcm",
],
}
],
stackIdx: [0, 2, 1],
stackPos: [0, 0, 2],
+104 -21
View File
@@ -232,7 +232,7 @@ const GROUPING_RULE_DEFINITIONS: GroupingRuleDefinition[] = [
id: "contentTime",
label: "Content/Acquisition time",
description: "Content Time (0008,0033) / Acquisition Time (0008,0032)",
defaultEnabled: true,
defaultEnabled: false,
},
{
id: "echoTime",
@@ -296,6 +296,66 @@ function parseNumericValueList(raw?: string | null): number[] {
.filter((value) => Number.isFinite(value));
}
/**
* Extract b-value(s) from a dicomParser DataSet. Handles multiple VR encodings:
* - (0018,9087) DiffusionBValue VR FD (double) in Enhanced MR, DS (string) in some classic MR
* - (0019,100C) GE private VR DS
* - (0043,1039) GE legacy VR IS/DS
* - (2001,1003) Philips private VR FL (32-bit float)
*/
function parseBValuesFromDataSet(dataSet: any): number[] {
if (!dataSet || typeof dataSet !== "object") return [];
const tryNumeric = (tagId: string): number | null => {
// DS/IS string form
if (typeof dataSet.string === "function") {
const raw = dataSet.string(tagId);
if (raw != null && raw !== "") {
const vals = parseNumericValueList(raw);
if (vals.length > 0 && Number.isFinite(vals[0]) && vals[0] >= 0) return vals[0];
}
}
// FD — 64-bit double (standard DiffusionBValue in Enhanced MR)
if (typeof dataSet.double === "function") {
try {
const v = dataSet.double(tagId);
if (Number.isFinite(v) && v >= 0) return v;
} catch (_) {}
}
// FL — 32-bit float (Philips private (2001,1003))
if (typeof dataSet.float === "function") {
try {
const v = dataSet.float(tagId);
if (Number.isFinite(v) && v >= 0) return v;
} catch (_) {}
}
return null;
};
// Standard (0018,9087) DiffusionBValue
const v1 = tryNumeric("x00189087");
if (v1 !== null) return [v1];
// GE private (0019,100C)
const v2 = tryNumeric("x0019100c");
if (v2 !== null) return [v2];
// GE legacy (0043,1039) — IS format, first value is b-value
if (typeof dataSet.string === "function") {
const raw = dataSet.string("x00431039");
if (raw) {
const parts = parseNumericValueList(raw);
if (parts.length > 0 && Number.isFinite(parts[0]) && parts[0] >= 0) return [parts[0]];
}
}
// Philips private (2001,1003) — VR FL
const v3 = tryNumeric("x20011003");
if (v3 !== null) return [v3];
return [];
}
function normalizeToWadouriOrExternal(imageId: string): string {
if (imageId.startsWith("wadouri:") || imageId.startsWith("external:")) {
return imageId;
@@ -1439,19 +1499,6 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
const cached = bValuesByBaseImageRef.current[baseUrl];
if (cached) return cached;
const parseBValuesFromDataSet = (dataSet: any): number[] => {
if (!dataSet || typeof dataSet.string !== "function") return [];
const standard = parseNumericValueList(dataSet.string("x00189087")); // DiffusionBValue
if (standard.length > 0) return standard;
const gePrivate = parseNumericValueList(dataSet.string("x0019100c"));
if (gePrivate.length > 0) return gePrivate;
const geLegacyPrivate = parseNumericValueList(dataSet.string("x00431039"));
if (geLegacyPrivate.length > 0) return geLegacyPrivate;
const privatePhilips = parseNumericValueList(dataSet.string("x20011003"));
if (privatePhilips.length > 0) return privatePhilips;
return [];
};
try {
const cacheManager = cornerstoneDICOMImageLoader?.wadouri?.dataSetCacheManager;
if (cacheManager?.get) {
@@ -1466,13 +1513,42 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
// Continue with fetch fallback.
}
// Helper: parse bytes tolerating truncation (header-only range requests)
const tryParseBytes = (bytes: Uint8Array): number[] => {
try {
return parseBValuesFromDataSet(dicomParser.parseDicom(bytes));
} catch (e: any) {
// dicomParser throws on truncation but attaches partial dataSet
if (e?.dataSet) {
const partial = parseBValuesFromDataSet(e.dataSet);
if (partial.length > 0) return partial;
}
return [];
}
};
// Range request — first 64 KB is enough to find b-value tags in the header
try {
const rangeResponse = await fetch(baseUrl, { headers: { Range: "bytes=0-65535" } });
if (rangeResponse.ok || rangeResponse.status === 206) {
const buf = await rangeResponse.arrayBuffer();
const result = tryParseBytes(new Uint8Array(buf));
if (result.length > 0 || rangeResponse.status === 200) {
bValuesByBaseImageRef.current[baseUrl] = result;
return result;
}
}
} catch (e) {
// Fall through to full fetch.
}
// Full fetch fallback (only if range request failed or server doesn't support Range)
try {
const response = await fetch(baseUrl);
const arrayBuffer = await response.arrayBuffer();
const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
const fromFetch = parseBValuesFromDataSet(dataSet);
bValuesByBaseImageRef.current[baseUrl] = fromFetch;
return fromFetch;
const buf = await response.arrayBuffer();
const result = tryParseBytes(new Uint8Array(buf));
bValuesByBaseImageRef.current[baseUrl] = result;
return result;
} catch (e) {
bValuesByBaseImageRef.current[baseUrl] = [];
return [];
@@ -1656,8 +1732,15 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
.map((candidate) => candidate.ruleValues[ruleId])
.filter((value): value is string => !!value);
const distinct = new Set(values);
// Ignore rules that are constant (or effectively unique per image) to avoid over-fragmentation.
return distinct.size > 1 && distinct.size < Math.max(2, candidates.length);
// Only activate when:
// 1. Multiple distinct values (not constant across the series)
// 2. Not effectively unique per-image (would produce a group of 1 per image)
// 3. At least 70% of images have a value for this rule — avoids fragmenting when
// only a handful of images returned metadata (e.g., b-value fetch failed for most)
const coverage = values.length / candidates.length;
return distinct.size > 1
&& distinct.size < Math.max(2, candidates.length)
&& coverage >= 0.7;
})
: [];