try to fix fluro
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Fluoro Multiframe Test</title>
|
||||
<style>
|
||||
html,body{height:100%;margin:0}
|
||||
.dicom-viewer-root{width:100%;height:100vh;background:#222;color:#fff}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main_viewer" class="dicom-viewer-root" data-auto-cache-stack="false"></div>
|
||||
|
||||
<!-- Provide noop React Refresh preamble to avoid plugin-react runtime errors -->
|
||||
<script>
|
||||
window.$RefreshReg$ = window.$RefreshReg$ || function() {};
|
||||
window.$RefreshSig$ = window.$RefreshSig$ || function() { return function(type) { return type; }; };
|
||||
</script>
|
||||
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
|
||||
<script>
|
||||
// Viewer state for fluoro multiframe images (using relative paths from public/test_images/)
|
||||
const state = {
|
||||
"grid": {"rows": 1, "cols": 2},
|
||||
"modes": ["stack", "stack"],
|
||||
"stacks": [
|
||||
{"i": ["wadouri:test_images/fluro/series_2369/1.3.12.2.1107.5.4.5.180506.30000026031914043350300000126.4.dcm"]},
|
||||
{"i": ["wadouri:test_images/multibvalues/IMG_792_og2P5S6.dcm", "wadouri:test_images/multibvalues/IMG_793_3tKkg87.dcm"]}
|
||||
],
|
||||
"stackIdx": [0, 1],
|
||||
"stackPos": [0, 1],
|
||||
"props": [
|
||||
{"voiRange": {"lower": 0, "upper": 500}, "invert": false, "interpolationType": 1, "VOILUTFunction": "LINEAR", "colormap": {"name": "Grayscale", "opacity": []}},
|
||||
{"voiRange": {"lower": 0, "upper": 500}, "invert": false, "interpolationType": 1, "VOILUTFunction": "LINEAR", "colormap": {"name": "Grayscale", "opacity": []}}
|
||||
],
|
||||
"cam": [
|
||||
{"viewUp": [0, 0, 1], "viewPlaneNormal": [1, 0, 0], "position": [150, 0, 0], "focalPoint": [0, 0, 0], "parallelProjection": true, "parallelScale": 100, "viewAngle": 90, "flipHorizontal": false, "flipVertical": false, "rotation": 0},
|
||||
{"viewUp": [0, 0, 1], "viewPlaneNormal": [1, 0, 0], "position": [150, 0, 0], "focalPoint": [0, 0, 0], "parallelProjection": true, "parallelScale": 100, "viewAngle": 90, "flipHorizontal": false, "flipVertical": false, "rotation": 0}
|
||||
],
|
||||
"orientations": [null, null],
|
||||
"volumeSliceIndices": [null, null]
|
||||
};
|
||||
|
||||
function callWhenReady(fnName, payload) {
|
||||
const maxWait = 5000;
|
||||
const start = Date.now();
|
||||
(function check() {
|
||||
if (window[fnName] && typeof window[fnName] === 'function') {
|
||||
try {
|
||||
window[fnName](payload);
|
||||
console.log('imported viewer state via', fnName);
|
||||
} catch (e) {
|
||||
console.error('importViewerState call failed', e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Date.now() - start > maxWait) {
|
||||
console.warn('Timed out waiting for', fnName);
|
||||
return;
|
||||
}
|
||||
setTimeout(check, 2000);
|
||||
})();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
callWhenReady('importViewerState', state);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -23,6 +23,7 @@
|
||||
<li><a href="/load-dir-test.html">load-dir-test.html</a> — directory loading test</li>
|
||||
<li><a href="/non-dicom-test.html">non-dicom-test.html</a> — non-DICOM handling test</li>
|
||||
<li><a href="/index.html">index.html</a> — project index</li>
|
||||
<li><a href="/fluoro-test.html">fluoro-test.html</a> — fluoro multiframe test</li>
|
||||
</ul>
|
||||
|
||||
<p>If you want these links added to the README, tell me and I will update it.</p>
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+301
-40
@@ -193,7 +193,23 @@ function withFrameQuery(imageId: string, frameNumber: number): string {
|
||||
if (hasFrameQuery(imageId)) {
|
||||
return imageId.replace(/([?&])frame=\d+/i, `$1frame=${frame}`);
|
||||
}
|
||||
return `${imageId}${imageId.includes("?") ? "&" : "?"}frame=${frame}`;
|
||||
// Cornerstone's wadouri multiframe path expects '&frame=' specifically.
|
||||
return `${imageId}&frame=${frame}`;
|
||||
}
|
||||
|
||||
function getFrameNumberFromImageId(imageId: string): number | null {
|
||||
const match = imageId.match(/[?&]frame=(\d+)/i);
|
||||
if (!match) return null;
|
||||
const parsed = Number(match[1]);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : null;
|
||||
}
|
||||
|
||||
function parseNumericValueList(raw?: string | null): number[] {
|
||||
if (!raw) return [];
|
||||
return raw
|
||||
.split(/\\|,/) // DICOM VM separator is '\\'; logs often show comma-separated values.
|
||||
.map((part) => Number(part.trim()))
|
||||
.filter((value) => Number.isFinite(value));
|
||||
}
|
||||
|
||||
function normalizeToWadouriOrExternal(imageId: string): string {
|
||||
@@ -376,7 +392,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
const [cineFpsByViewport, setCineFpsByViewport] = useState<number[]>(
|
||||
() => Array(MAX_VIEWPORTS).fill(12)
|
||||
);
|
||||
const [cineControlsOpenByViewport, setCineControlsOpenByViewport] = useState<boolean[]>(
|
||||
() => Array(MAX_VIEWPORTS).fill(false)
|
||||
);
|
||||
const multiframeCountCacheRef = useRef<Record<string, number>>({});
|
||||
const bValuesByBaseImageRef = useRef<Record<string, number[]>>({});
|
||||
|
||||
// Client-side generated thumbnails cache: stackIndex -> dataURL
|
||||
const [stackThumbnails, setStackThumbnails] = useState<Record<number, string>>({});
|
||||
@@ -757,7 +777,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
stacksRaw.map(async (d) => {
|
||||
if (Array.isArray(d)) {
|
||||
const expanded = await expandMultiframeImageIds(d as string[]);
|
||||
return { imageIds: expanded };
|
||||
const seriesGroups = await deriveSeriesGroupsFromImageIds(expanded);
|
||||
return { imageIds: expanded, seriesGroups };
|
||||
}
|
||||
const imageIdsRaw = Array.isArray((d as any).imageIds) ? (d as any).imageIds : (Array.isArray((d as any).i) ? (d as any).i : []);
|
||||
const imageIds = await expandMultiframeImageIds(imageIdsRaw);
|
||||
@@ -788,6 +809,10 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
}
|
||||
}
|
||||
|
||||
if (!seriesGroups || seriesGroups.length === 0) {
|
||||
seriesGroups = await deriveSeriesGroupsFromImageIds(imageIds);
|
||||
}
|
||||
|
||||
// try to preserve old studyInstanceUID extraction if needed (best-effort)
|
||||
let studyInstanceUID: string | undefined = (d as any).studyInstanceUID;
|
||||
// optional: attempt to fetch first file and extract StudyInstanceUID if dicomParser available
|
||||
@@ -821,10 +846,17 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
// Seed viewports with first stack if present (preserve old behavior)
|
||||
if (normalized[0]) {
|
||||
const firstStack = normalized[0];
|
||||
const firstSeries = firstStack.seriesGroups?.[0]?.imageIds || firstStack.imageIds;
|
||||
let firstGroups = firstStack.seriesGroups || [];
|
||||
if (firstGroups.length === 0) {
|
||||
await setLoadedImageIdsAndCacheMeta(firstStack.imageIds, { force: true });
|
||||
firstGroups = await deriveSeriesGroupsFromImageIds(firstStack.imageIds);
|
||||
normalized[0] = { ...firstStack, seriesGroups: firstGroups };
|
||||
setAvailableStacks([...normalized]);
|
||||
}
|
||||
const firstSeries = firstGroups[0]?.imageIds || firstStack.imageIds;
|
||||
await setLoadedImageIdsAndCacheMeta(firstSeries);
|
||||
setViewportImageIds(Array(MAX_VIEWPORTS).fill(firstSeries || []));
|
||||
setSeriesGroupsByViewport(Array(MAX_VIEWPORTS).fill(null).map(() => firstStack.seriesGroups || []));
|
||||
setSeriesGroupsByViewport(Array(MAX_VIEWPORTS).fill(null).map(() => firstGroups));
|
||||
setSelectedSeriesIdxByViewport(Array(MAX_VIEWPORTS).fill(0));
|
||||
} else {
|
||||
setViewportImageIds(Array(MAX_VIEWPORTS).fill([]));
|
||||
@@ -844,21 +876,26 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
const handleLoadStack = async (viewportIdx: number, stackIdx: number) => {
|
||||
const stackObj = availableStacks[stackIdx];
|
||||
if (!stackObj) return;
|
||||
let seriesGroups = stackObj.seriesGroups;
|
||||
const stack = stackObj.imageIds;
|
||||
let stack = stackObj.imageIds;
|
||||
console.log("[handleLoadStack] Starting with imageIds:", stack.length, "ids. First:", stack[0]?.slice(0, 80));
|
||||
|
||||
// IMPORTANT: Expand multiframe images BEFORE caching metadata
|
||||
const expandedStack = await expandMultiframeImageIds(stack);
|
||||
console.log("[handleLoadStack] After expansion:", expandedStack.length, "ids. First:", expandedStack[0]?.slice(0, 80));
|
||||
stack = expandedStack;
|
||||
|
||||
await setLoadedImageIdsAndCacheMeta(stack, { force: true });
|
||||
if (!seriesGroups || seriesGroups.length === 0) {
|
||||
seriesGroups = deriveSeriesGroupsFromImageIds(stack);
|
||||
const seriesGroups = await deriveSeriesGroupsFromImageIds(stack);
|
||||
console.log("[handleLoadStack] Derived series groups:", seriesGroups.length, "groups. First group has", seriesGroups[0]?.imageIds.length, "images");
|
||||
setAvailableStacks(prev => {
|
||||
const next = [...prev];
|
||||
if (!next[stackIdx]) return prev;
|
||||
next[stackIdx] = { ...next[stackIdx], seriesGroups };
|
||||
next[stackIdx] = { ...next[stackIdx], seriesGroups, imageIds: stack };
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
const primarySeries = seriesGroups[0]?.imageIds || stack;
|
||||
console.log("[handleLoadStack] Setting viewport to primarySeries with", primarySeries.length, "ids. First:", primarySeries[0]?.slice(0, 80));
|
||||
|
||||
setLoadingState("displayset");
|
||||
try {
|
||||
@@ -1129,10 +1166,21 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
const shouldCache = Boolean(autoCacheStack || options?.force);
|
||||
|
||||
if (shouldCache) {
|
||||
const urlsToLoad = new Set<string>();
|
||||
for (const imageId of imageIds) {
|
||||
if (imageId.startsWith("wadouri:")) {
|
||||
try {
|
||||
const url = stripWadouriPrefix(imageId);
|
||||
const baseUrl = stripWadouriPrefix(imageId).replace(/[?&]frame=\d+/gi, "");
|
||||
if (baseUrl) {
|
||||
urlsToLoad.add(baseUrl);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Failed to cache metadata for ${imageId}:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const url of urlsToLoad) {
|
||||
if (cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager) {
|
||||
// @ts-ignore
|
||||
const result = cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.load(url);
|
||||
@@ -1142,10 +1190,6 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
} else {
|
||||
console.warn("MetaDataManager not found. Unable to cache metadata.");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Failed to cache metadata for ${imageId}:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all metadata loads to finish
|
||||
@@ -1169,6 +1213,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
const cacheKey = `wadouri:${url}`;
|
||||
const cached = multiframeCountCacheRef.current[cacheKey];
|
||||
if (cached !== undefined) {
|
||||
console.log("[getNumberOfFramesForImageId] Cache hit:", cacheKey.slice(0, 60), "->", cached);
|
||||
return cached;
|
||||
}
|
||||
|
||||
@@ -1182,6 +1227,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
const multiframeMeta = metaData.get("multiframeModule", normalized) as any;
|
||||
const fromMeta = Number(multiframeMeta?.numberOfFrames);
|
||||
if (Number.isFinite(fromMeta) && fromMeta > 1) {
|
||||
console.log("[getNumberOfFramesForImageId] Found in metadata:", fromMeta);
|
||||
multiframeCountCacheRef.current[cacheKey] = Math.floor(fromMeta);
|
||||
return Math.floor(fromMeta);
|
||||
}
|
||||
@@ -1194,6 +1240,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
if (cacheManager?.get) {
|
||||
const dataSet = cacheManager.get(url);
|
||||
const fromCache = readFramesFromDataSet(dataSet);
|
||||
console.log("[getNumberOfFramesForImageId] Found in cache manager:", fromCache);
|
||||
multiframeCountCacheRef.current[cacheKey] = fromCache;
|
||||
return fromCache;
|
||||
}
|
||||
@@ -1202,13 +1249,16 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("[getNumberOfFramesForImageId] Fetching:", url.slice(0, 80));
|
||||
const response = await fetch(url);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
||||
const fromFetch = readFramesFromDataSet(dataSet);
|
||||
console.log("[getNumberOfFramesForImageId] Found via fetch:", fromFetch);
|
||||
multiframeCountCacheRef.current[cacheKey] = fromFetch;
|
||||
return fromFetch;
|
||||
} catch (e) {
|
||||
console.log("[getNumberOfFramesForImageId] Fetch failed, returning 1:", e);
|
||||
multiframeCountCacheRef.current[cacheKey] = 1;
|
||||
return 1;
|
||||
}
|
||||
@@ -1216,6 +1266,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
|
||||
const expandMultiframeImageIds = useCallback(async (rawImageIds: string[]) => {
|
||||
const expanded: string[] = [];
|
||||
console.log("[expandMultiframeImageIds] Input:", rawImageIds.length, "imageIds");
|
||||
|
||||
for (const raw of rawImageIds || []) {
|
||||
const imageId = normalizeToWadouriOrExternal(raw);
|
||||
@@ -1225,6 +1276,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
}
|
||||
|
||||
const frameCount = await getNumberOfFramesForImageId(imageId);
|
||||
console.log("[expandMultiframeImageIds] frameCount for", imageId.slice(0, 80), ":", frameCount);
|
||||
if (frameCount <= 1) {
|
||||
expanded.push(imageId);
|
||||
continue;
|
||||
@@ -1233,16 +1285,87 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
for (let frame = 1; frame <= frameCount; frame++) {
|
||||
expanded.push(withFrameQuery(imageId, frame));
|
||||
}
|
||||
console.log("[expandMultiframeImageIds] Expanded to", frameCount, "frames");
|
||||
}
|
||||
|
||||
return dedupeImageIds(expanded);
|
||||
const result = dedupeImageIds(expanded);
|
||||
console.log("[expandMultiframeImageIds] Output:", result.length, "total imageIds");
|
||||
return result;
|
||||
}, [getNumberOfFramesForImageId]);
|
||||
|
||||
const deriveSeriesGroupsFromImageIds = useCallback((imageIds: string[]): StackSeriesGroup[] => {
|
||||
const getFallbackBValuesForImageId = useCallback(async (imageId: string): Promise<number[]> => {
|
||||
if (!imageId.startsWith("wadouri:")) return [];
|
||||
|
||||
const normalized = normalizeToWadouriOrExternal(imageId);
|
||||
const baseUrl = stripWadouriPrefix(normalized).replace(/[?&]frame=\d+/i, "");
|
||||
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 privatePhilips = parseNumericValueList(dataSet.string("x20011003"));
|
||||
if (privatePhilips.length > 0) return privatePhilips;
|
||||
return [];
|
||||
};
|
||||
|
||||
try {
|
||||
const cacheManager = cornerstoneDICOMImageLoader?.wadouri?.dataSetCacheManager;
|
||||
if (cacheManager?.get) {
|
||||
const existing = cacheManager.get(baseUrl);
|
||||
const fromExisting = parseBValuesFromDataSet(existing);
|
||||
if (fromExisting.length > 0) {
|
||||
bValuesByBaseImageRef.current[baseUrl] = fromExisting;
|
||||
return fromExisting;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Continue with fetch fallback.
|
||||
}
|
||||
|
||||
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;
|
||||
} catch (e) {
|
||||
bValuesByBaseImageRef.current[baseUrl] = [];
|
||||
return [];
|
||||
}
|
||||
}, []);
|
||||
|
||||
const deriveSeriesGroupsFromImageIds = useCallback(async (imageIds: string[]): Promise<StackSeriesGroup[]> => {
|
||||
if (!Array.isArray(imageIds) || imageIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const frameOrderByBase = new Map<string, string[]>();
|
||||
imageIds.forEach((id) => {
|
||||
const base = stripWadouriPrefix(normalizeToWadouriOrExternal(id)).replace(/[?&]frame=\d+/i, "");
|
||||
const arr = frameOrderByBase.get(base) || [];
|
||||
arr.push(id);
|
||||
frameOrderByBase.set(base, arr);
|
||||
});
|
||||
|
||||
frameOrderByBase.forEach((arr, base) => {
|
||||
arr.sort((a, b) => {
|
||||
const fa = getFrameNumberFromImageId(a) || 1;
|
||||
const fb = getFrameNumberFromImageId(b) || 1;
|
||||
return fa - fb;
|
||||
});
|
||||
frameOrderByBase.set(base, arr);
|
||||
});
|
||||
|
||||
const fallbackByBase = new Map<string, number[]>();
|
||||
await Promise.all(Array.from(frameOrderByBase.keys()).map(async (base) => {
|
||||
const firstId = `wadouri:${base}`;
|
||||
const values = await getFallbackBValuesForImageId(firstId);
|
||||
fallbackByBase.set(base, values);
|
||||
}));
|
||||
|
||||
const groups = new Map<string, { imageIds: string[]; seriesInstanceUID?: string; seriesDescription?: string; seriesNumber?: number; bValue?: number | null }>();
|
||||
|
||||
imageIds.forEach((id) => {
|
||||
@@ -1255,8 +1378,33 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
const seriesNumber = Number(seriesMeta.seriesNumber);
|
||||
const temporalPosition = Number(imageMeta.temporalPositionIdentifier);
|
||||
const triggerTime = Number(imageMeta.triggerTime);
|
||||
const bValueRaw = Number(mrMeta.diffusionBValue ?? mrMeta.diffusionbValue ?? mrMeta.bValue);
|
||||
const bValue = Number.isFinite(bValueRaw) ? bValueRaw : null;
|
||||
|
||||
let bValueRaw = Number(mrMeta.diffusionBValue ?? mrMeta.diffusionbValue ?? mrMeta.bValue);
|
||||
let bValue = Number.isFinite(bValueRaw) ? bValueRaw : null;
|
||||
|
||||
if (bValue === null) {
|
||||
const base = stripWadouriPrefix(normalizeToWadouriOrExternal(id)).replace(/[?&]frame=\d+/i, "");
|
||||
const fallbackValues = fallbackByBase.get(base) || [];
|
||||
if (fallbackValues.length > 0) {
|
||||
const orderedIds = frameOrderByBase.get(base) || [id];
|
||||
const framePos = Math.max(0, orderedIds.indexOf(id));
|
||||
const totalForBase = Math.max(1, orderedIds.length);
|
||||
|
||||
if (fallbackValues.length === 1) {
|
||||
bValue = fallbackValues[0];
|
||||
} else if (fallbackValues.length === totalForBase) {
|
||||
bValue = fallbackValues[Math.min(fallbackValues.length - 1, framePos)];
|
||||
} else if (fallbackValues.length === 2 && totalForBase % 2 === 0) {
|
||||
bValue = framePos < totalForBase / 2 ? fallbackValues[0] : fallbackValues[1];
|
||||
} else {
|
||||
const bucket = Math.min(
|
||||
fallbackValues.length - 1,
|
||||
Math.floor((framePos / totalForBase) * fallbackValues.length)
|
||||
);
|
||||
bValue = fallbackValues[bucket];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const temporalKey = Number.isFinite(temporalPosition) ? `t${temporalPosition}` : "";
|
||||
const triggerKey = Number.isFinite(triggerTime) ? `tr${Math.round(triggerTime)}` : "";
|
||||
@@ -1298,7 +1446,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
});
|
||||
|
||||
return ordered;
|
||||
}, []);
|
||||
}, [getFallbackBValuesForImageId]);
|
||||
|
||||
|
||||
const [viewportGrid, setViewportGrid] = useState({ rows: 1, cols: 1 })
|
||||
@@ -1364,6 +1512,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
resolution: string,
|
||||
spacing: string,
|
||||
focalPoint: string,
|
||||
frameIndex?: number,
|
||||
totalFrames?: number,
|
||||
}>>(() =>
|
||||
Array(viewportGrid.rows * viewportGrid.cols).fill({
|
||||
index: 0,
|
||||
@@ -1961,17 +2111,12 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
}
|
||||
return frames;
|
||||
});
|
||||
const seriesGroups = await deriveSeriesGroupsFromImageIds(imageIds);
|
||||
stacks.push({
|
||||
imageIds,
|
||||
studyInstanceUID: g.studyInstanceUID,
|
||||
seriesInstanceUID: g.seriesInstanceUID,
|
||||
seriesGroups: [{
|
||||
key: String(g.seriesInstanceUID || key),
|
||||
label: `Series ${stacks.length + 1}`,
|
||||
imageIds,
|
||||
seriesInstanceUID: g.seriesInstanceUID,
|
||||
bValue: null,
|
||||
}],
|
||||
seriesGroups,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2086,6 +2231,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
let resolution = "";
|
||||
let spacing = "";
|
||||
let focalPoint = "";
|
||||
let frameIndex = undefined;
|
||||
let totalFrames = undefined;
|
||||
|
||||
if (viewportModes[viewportIdx] === "stack") {
|
||||
currentIndex = typeof viewport.getCurrentImageIdIndex === "function"
|
||||
@@ -2098,6 +2245,35 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
|
||||
const currentImageId = imageIds[currentIndex];
|
||||
if (currentImageId) {
|
||||
console.log("[updateOverlay] Current image:", currentImageId.slice(0, 80), "frameIndex status:", frameIndex, "totalFrames:", totalFrames);
|
||||
// Detect frame index from imageId query
|
||||
if (currentImageId.includes('&frame=')) {
|
||||
const match = currentImageId.match(/&frame=(\d+)/);
|
||||
if (match) {
|
||||
frameIndex = parseInt(match[1], 10);
|
||||
console.log("[updateOverlay] Frame detected:", frameIndex);
|
||||
// Try to get total frames from metadata
|
||||
const baseImageId = currentImageId.split('&frame=')[0];
|
||||
const multiframeMeta = metaData.get("multiframeModule", baseImageId) as any;
|
||||
if (multiframeMeta?.numberOfFrames) {
|
||||
totalFrames = multiframeMeta.numberOfFrames;
|
||||
console.log("[updateOverlay] Total frames from metadata:", totalFrames);
|
||||
} else {
|
||||
// Fallback: count unique frame indices in imageIds
|
||||
const frameNumbers = imageIds.map(id => {
|
||||
const m = id.match(/&frame=(\d+)/);
|
||||
return m ? parseInt(m[1], 10) : null;
|
||||
}).filter(n => n !== null && typeof n === 'number');
|
||||
if (frameNumbers.length > 0) {
|
||||
totalFrames = Math.max(...frameNumbers as number[]);
|
||||
console.log("[updateOverlay] Total frames from imageIds:", totalFrames);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log("[updateOverlay] No frame query in imageId. imageIds.length:", imageIds.length);
|
||||
}
|
||||
|
||||
const seriesMeta = metaData.get("generalSeriesModule", currentImageId) as any;
|
||||
const planeMeta = metaData.get("imagePlaneModule", currentImageId) as any;
|
||||
const pixelMeta = metaData.get("imagePixelModule", currentImageId) as any;
|
||||
@@ -2222,6 +2398,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
resolution,
|
||||
spacing,
|
||||
focalPoint,
|
||||
frameIndex,
|
||||
totalFrames,
|
||||
};
|
||||
return next;
|
||||
});
|
||||
@@ -2278,7 +2456,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
const imageIds = await expandMultiframeImageIds(rawImageIds.map(normalizeToWadouriOrExternal));
|
||||
if (imageIds.length === 0) return;
|
||||
await setLoadedImageIdsAndCacheMeta(imageIds, { force: true });
|
||||
const seriesGroups = deriveSeriesGroupsFromImageIds(imageIds);
|
||||
const seriesGroups = await deriveSeriesGroupsFromImageIds(imageIds);
|
||||
const primarySeries = seriesGroups[0]?.imageIds || imageIds;
|
||||
|
||||
// Add the incoming stack to the panel and capture its index.
|
||||
@@ -2393,19 +2571,20 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
const applyStackDrop = async (viewportIdx: number, stackIdx: number, requestedZone: ViewportDropZone) => {
|
||||
const stackObj = availableStacks[stackIdx];
|
||||
if (!stackObj) return;
|
||||
let seriesGroups = stackObj.seriesGroups;
|
||||
const stackImageIds = stackObj.imageIds || [];
|
||||
let stackImageIds = stackObj.imageIds || [];
|
||||
|
||||
// IMPORTANT: Expand multiframe images BEFORE caching metadata
|
||||
const expandedImageIds = await expandMultiframeImageIds(stackImageIds);
|
||||
stackImageIds = expandedImageIds;
|
||||
|
||||
await setLoadedImageIdsAndCacheMeta(stackImageIds, { force: true });
|
||||
if (!seriesGroups || seriesGroups.length === 0) {
|
||||
seriesGroups = deriveSeriesGroupsFromImageIds(stackImageIds);
|
||||
const seriesGroups = await deriveSeriesGroupsFromImageIds(stackImageIds);
|
||||
setAvailableStacks(prev => {
|
||||
const next = [...prev];
|
||||
if (!next[stackIdx]) return prev;
|
||||
next[stackIdx] = { ...next[stackIdx], seriesGroups };
|
||||
next[stackIdx] = { ...next[stackIdx], seriesGroups, imageIds: stackImageIds };
|
||||
return next;
|
||||
});
|
||||
}
|
||||
const primarySeries = seriesGroups[0]?.imageIds || stackImageIds;
|
||||
|
||||
const resolvedZone = resolveDropZone(requestedZone);
|
||||
@@ -3238,6 +3417,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
focalPoint: "",
|
||||
}));
|
||||
setPresetsOpenArr(Array(viewportGrid.rows * viewportGrid.cols).fill(false));
|
||||
setCineControlsOpenByViewport(Array(MAX_VIEWPORTS).fill(false));
|
||||
}, [viewportGrid]);
|
||||
|
||||
// Re-apply bindings when tool sets or ctrl changes
|
||||
@@ -3324,7 +3504,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
if (!wadouriImageIds || wadouriImageIds.length === 0) return;
|
||||
const expandedImageIds = await expandMultiframeImageIds(wadouriImageIds);
|
||||
await setLoadedImageIdsAndCacheMeta(expandedImageIds, { force: true });
|
||||
const seriesGroups = deriveSeriesGroupsFromImageIds(expandedImageIds);
|
||||
const seriesGroups = await deriveSeriesGroupsFromImageIds(expandedImageIds);
|
||||
const primarySeries = seriesGroups[0]?.imageIds || expandedImageIds;
|
||||
|
||||
// Try to extract StudyInstanceUID from the first image if possible
|
||||
@@ -3496,6 +3676,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
setSeriesGroupsByViewport(Array(MAX_VIEWPORTS).fill(null).map(() => []));
|
||||
setSelectedSeriesIdxByViewport(Array(MAX_VIEWPORTS).fill(0));
|
||||
setCinePlayingByViewport(Array(MAX_VIEWPORTS).fill(false));
|
||||
setCineControlsOpenByViewport(Array(MAX_VIEWPORTS).fill(false));
|
||||
setViewportModes(Array(MAX_VIEWPORTS).fill('stack'));
|
||||
Object.values(stackThumbnailsRef.current).forEach((thumbnail) => {
|
||||
if (typeof thumbnail === 'string' && thumbnail.startsWith('blob:')) {
|
||||
@@ -3612,7 +3793,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
if (!Array.isArray(imageIds) || imageIds.length === 0) return;
|
||||
const expandedImageIds = await expandMultiframeImageIds(imageIds);
|
||||
await setLoadedImageIdsAndCacheMeta(expandedImageIds, { force: true });
|
||||
const seriesGroups = deriveSeriesGroupsFromImageIds(expandedImageIds);
|
||||
const seriesGroups = await deriveSeriesGroupsFromImageIds(expandedImageIds);
|
||||
const primarySeries = seriesGroups[0]?.imageIds || expandedImageIds;
|
||||
// Optionally extract StudyInstanceUID if not provided
|
||||
let uid = studyInstanceUID;
|
||||
@@ -4755,6 +4936,9 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
<>
|
||||
<div>
|
||||
Image: {imageInfos[i]?.index} / {imageInfos[i]?.total}
|
||||
{imageInfos[i]?.frameIndex && imageInfos[i]?.totalFrames ? (
|
||||
<> [Frame: {imageInfos[i]?.frameIndex} / {imageInfos[i]?.totalFrames}]</>
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
WC: {imageInfos[i]?.windowCenter} WW: {imageInfos[i]?.windowWidth}
|
||||
@@ -4812,8 +4996,43 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
bottom: 8,
|
||||
zIndex: 110, // above the bottom study selector (zIndex:20)
|
||||
pointerEvents: "auto", // <-- ensure pointer events are enabled
|
||||
display: "flex",
|
||||
alignItems: "flex-end",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
style={{
|
||||
background: cineControlsOpenByViewport[i] ? "rgba(30,120,200,0.95)" : "rgba(0,0,0,0.7)",
|
||||
color: "#fff",
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
borderRadius: "4px",
|
||||
fontSize: "12px",
|
||||
fontWeight: 700,
|
||||
padding: "6px 9px",
|
||||
cursor: "pointer",
|
||||
minHeight: 30,
|
||||
}}
|
||||
title={cineControlsOpenByViewport[i] ? "Hide cine controls" : "Show cine controls"}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setCineControlsOpenByViewport((prev) => {
|
||||
const next = [...prev];
|
||||
const nextOpen = !next[i];
|
||||
next[i] = nextOpen;
|
||||
if (!nextOpen) {
|
||||
setCinePlayingByViewport((playing) => {
|
||||
const p = [...playing];
|
||||
p[i] = false;
|
||||
return p;
|
||||
});
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
>
|
||||
Cine
|
||||
</button>
|
||||
<div
|
||||
className="preset-menu"
|
||||
style={{
|
||||
@@ -4913,7 +5132,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{viewportModes[i] === "stack" && (
|
||||
{viewportModes[i] === "stack" && (cineControlsOpenByViewport[i] || (seriesGroupsByViewport[i] || []).length > 1 || (imageInfos[i]?.frameIndex && imageInfos[i]?.totalFrames)) && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
@@ -4934,7 +5153,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{imageInfos[i]?.total > 1 && (
|
||||
{cineControlsOpenByViewport[i] && imageInfos[i]?.total > 1 && (
|
||||
<>
|
||||
<button
|
||||
style={{
|
||||
@@ -4978,6 +5197,43 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
{imageInfos[i]?.frameIndex && imageInfos[i]?.totalFrames && imageInfos[i]?.totalFrames > 1 && (
|
||||
<>
|
||||
<span style={{ color: "#fff", fontSize: 12, whiteSpace: "nowrap" }}>Frame</span>
|
||||
<input
|
||||
type="range"
|
||||
min={1}
|
||||
max={imageInfos[i]?.totalFrames || 1}
|
||||
step={1}
|
||||
value={Math.max(1, Math.min(imageInfos[i]?.totalFrames || 1, imageInfos[i]?.frameIndex || 1))}
|
||||
onChange={(e) => {
|
||||
const targetFrame = Number(e.target.value);
|
||||
const imageIds = viewportImageIds[i] || [];
|
||||
const targetIdx = imageIds.findIndex(id => {
|
||||
const m = id.match(/&frame=(\d+)/);
|
||||
return m && parseInt(m[1], 10) === targetFrame;
|
||||
});
|
||||
if (targetIdx >= 0) {
|
||||
const viewport = renderingEngineRef.current?.getViewport(`CT_${i}`);
|
||||
if (viewport && typeof csUtilities.jumpToSlice === "function") {
|
||||
csUtilities.jumpToSlice(viewport.element, { imageIndex: targetIdx });
|
||||
}
|
||||
}
|
||||
}}
|
||||
style={{ width: 100 }}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
color: "#e0e0e0",
|
||||
fontSize: 11,
|
||||
minWidth: 50,
|
||||
textAlign: "right",
|
||||
}}
|
||||
>
|
||||
{imageInfos[i]?.frameIndex} / {imageInfos[i]?.totalFrames}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{(seriesGroupsByViewport[i] || []).length > 1 && (
|
||||
<>
|
||||
<span style={{ color: "#fff", fontSize: 12, whiteSpace: "nowrap" }}>Series</span>
|
||||
@@ -5239,7 +5495,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: viewportModes[i] === "stack" ? 56 : 8,
|
||||
bottom: viewportModes[i] === "stack" && (cineControlsOpenByViewport[i] || (seriesGroupsByViewport[i] || []).length > 1) ? 56 : 8,
|
||||
zIndex: 20,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
@@ -5340,8 +5596,13 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
}
|
||||
}}
|
||||
onBlur={e => {
|
||||
const blurHost = e.currentTarget as HTMLElement | null;
|
||||
setTimeout(() => {
|
||||
if (!(e.currentTarget as HTMLElement).contains(document.activeElement)) {
|
||||
if (!blurHost) {
|
||||
setOpenDropdownIdx(null);
|
||||
return;
|
||||
}
|
||||
if (!blurHost.contains(document.activeElement)) {
|
||||
setOpenDropdownIdx(null);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
Reference in New Issue
Block a user