feat(b-values): implement parsing for multiple b-value formats and update default setting
This commit is contained in:
@@ -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
@@ -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;
|
||||
})
|
||||
: [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user