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",
|
"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],
|
stackIdx: [0, 2, 1],
|
||||||
stackPos: [0, 0, 2],
|
stackPos: [0, 0, 2],
|
||||||
|
|||||||
+104
-21
@@ -232,7 +232,7 @@ const GROUPING_RULE_DEFINITIONS: GroupingRuleDefinition[] = [
|
|||||||
id: "contentTime",
|
id: "contentTime",
|
||||||
label: "Content/Acquisition time",
|
label: "Content/Acquisition time",
|
||||||
description: "Content Time (0008,0033) / Acquisition Time (0008,0032)",
|
description: "Content Time (0008,0033) / Acquisition Time (0008,0032)",
|
||||||
defaultEnabled: true,
|
defaultEnabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "echoTime",
|
id: "echoTime",
|
||||||
@@ -296,6 +296,66 @@ function parseNumericValueList(raw?: string | null): number[] {
|
|||||||
.filter((value) => Number.isFinite(value));
|
.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 {
|
function normalizeToWadouriOrExternal(imageId: string): string {
|
||||||
if (imageId.startsWith("wadouri:") || imageId.startsWith("external:")) {
|
if (imageId.startsWith("wadouri:") || imageId.startsWith("external:")) {
|
||||||
return imageId;
|
return imageId;
|
||||||
@@ -1439,19 +1499,6 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
|||||||
const cached = bValuesByBaseImageRef.current[baseUrl];
|
const cached = bValuesByBaseImageRef.current[baseUrl];
|
||||||
if (cached) return cached;
|
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 {
|
try {
|
||||||
const cacheManager = cornerstoneDICOMImageLoader?.wadouri?.dataSetCacheManager;
|
const cacheManager = cornerstoneDICOMImageLoader?.wadouri?.dataSetCacheManager;
|
||||||
if (cacheManager?.get) {
|
if (cacheManager?.get) {
|
||||||
@@ -1466,13 +1513,42 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
|||||||
// Continue with fetch fallback.
|
// 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 {
|
try {
|
||||||
const response = await fetch(baseUrl);
|
const response = await fetch(baseUrl);
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
const buf = await response.arrayBuffer();
|
||||||
const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
const result = tryParseBytes(new Uint8Array(buf));
|
||||||
const fromFetch = parseBValuesFromDataSet(dataSet);
|
bValuesByBaseImageRef.current[baseUrl] = result;
|
||||||
bValuesByBaseImageRef.current[baseUrl] = fromFetch;
|
return result;
|
||||||
return fromFetch;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
bValuesByBaseImageRef.current[baseUrl] = [];
|
bValuesByBaseImageRef.current[baseUrl] = [];
|
||||||
return [];
|
return [];
|
||||||
@@ -1656,8 +1732,15 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
|||||||
.map((candidate) => candidate.ruleValues[ruleId])
|
.map((candidate) => candidate.ruleValues[ruleId])
|
||||||
.filter((value): value is string => !!value);
|
.filter((value): value is string => !!value);
|
||||||
const distinct = new Set(values);
|
const distinct = new Set(values);
|
||||||
// Ignore rules that are constant (or effectively unique per image) to avoid over-fragmentation.
|
// Only activate when:
|
||||||
return distinct.size > 1 && distinct.size < Math.max(2, candidates.length);
|
// 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