nuxt-cornerstone 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +933 -0
- package/dist/module.d.mts +22 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +168 -0
- package/dist/runtime/annotation-json.d.ts +76 -0
- package/dist/runtime/annotation-json.js +176 -0
- package/dist/runtime/components/CornerstoneViewport.d.vue.ts +57 -0
- package/dist/runtime/components/CornerstoneViewport.vue +227 -0
- package/dist/runtime/components/CornerstoneViewport.vue.d.ts +57 -0
- package/dist/runtime/composables/useAnnotationReport.d.ts +28 -0
- package/dist/runtime/composables/useAnnotationReport.js +66 -0
- package/dist/runtime/composables/useCinePlayer.d.ts +52 -0
- package/dist/runtime/composables/useCinePlayer.js +95 -0
- package/dist/runtime/composables/useCornerstone.d.ts +17 -0
- package/dist/runtime/composables/useCornerstone.js +30 -0
- package/dist/runtime/composables/useCornerstoneI18n.d.ts +24 -0
- package/dist/runtime/composables/useCornerstoneI18n.js +32 -0
- package/dist/runtime/composables/useCornerstoneTools.d.ts +21 -0
- package/dist/runtime/composables/useCornerstoneTools.js +105 -0
- package/dist/runtime/composables/useDicomAnnotations.d.ts +76 -0
- package/dist/runtime/composables/useDicomAnnotations.js +220 -0
- package/dist/runtime/composables/useDicomFiles.d.ts +87 -0
- package/dist/runtime/composables/useDicomFiles.js +234 -0
- package/dist/runtime/composables/useDicomGuard.d.ts +24 -0
- package/dist/runtime/composables/useDicomGuard.js +30 -0
- package/dist/runtime/composables/useDicomStudy.d.ts +122 -0
- package/dist/runtime/composables/useDicomStudy.js +222 -0
- package/dist/runtime/composables/useImagePrefetch.d.ts +78 -0
- package/dist/runtime/composables/useImagePrefetch.js +119 -0
- package/dist/runtime/composables/useMeasurements.d.ts +51 -0
- package/dist/runtime/composables/useMeasurements.js +138 -0
- package/dist/runtime/composables/useRenderingEngine.d.ts +6 -0
- package/dist/runtime/composables/useRenderingEngine.js +46 -0
- package/dist/runtime/composables/useStackCine.d.ts +50 -0
- package/dist/runtime/composables/useStackCine.js +62 -0
- package/dist/runtime/composables/useViewerShortcuts.d.ts +94 -0
- package/dist/runtime/composables/useViewerShortcuts.js +115 -0
- package/dist/runtime/cornerstone.d.ts +21 -0
- package/dist/runtime/cornerstone.js +100 -0
- package/dist/runtime/dicom-instances.d.ts +32 -0
- package/dist/runtime/dicom-instances.js +15 -0
- package/dist/runtime/dicom-zip.d.ts +95 -0
- package/dist/runtime/dicom-zip.js +149 -0
- package/dist/runtime/i18n/detect.d.ts +37 -0
- package/dist/runtime/i18n/detect.js +37 -0
- package/dist/runtime/i18n/index.d.ts +61 -0
- package/dist/runtime/i18n/index.js +145 -0
- package/dist/runtime/i18n/messages.d.ts +122 -0
- package/dist/runtime/i18n/messages.js +147 -0
- package/dist/runtime/plugin.client.d.ts +13 -0
- package/dist/runtime/plugin.client.js +31 -0
- package/dist/runtime/plugin.i18n.d.ts +12 -0
- package/dist/runtime/plugin.i18n.js +15 -0
- package/dist/runtime/types.d.ts +150 -0
- package/dist/runtime/types.js +10 -0
- package/dist/types.d.mts +29 -0
- package/package.json +76 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
import { CornerstoneModuleOptions } from '../dist/runtime/types.js';
|
|
3
|
+
export { CornerstoneI18nOptions, CornerstoneModuleOptions, LocaleSource, ResolvedCornerstoneOptions } from '../dist/runtime/types.js';
|
|
4
|
+
export { CornerstoneMessageKey, CornerstoneTranslator, MessageCatalog, MessageKey, MessageParams, MessageValue, PluralCategory } from '../dist/runtime/i18n/index.js';
|
|
5
|
+
export { AddFilesOptions, AddZipOptions, AddZipResult, DicomSeries, IndexUrlsOptions, IndexUrlsResult, SkipReason, SkippedEntry, ZipProgress } from '../dist/runtime/composables/useDicomFiles.js';
|
|
6
|
+
export { PrefetchOptions, PrefetchProgress, PrefetchResult } from '../dist/runtime/composables/useImagePrefetch.js';
|
|
7
|
+
export { CinePlayerOptions } from '../dist/runtime/composables/useCinePlayer.js';
|
|
8
|
+
export { OpenUrlsOptions, Problem, SourceInfo } from '../dist/runtime/composables/useDicomStudy.js';
|
|
9
|
+
export { GuardResult, RejectedFile } from '../dist/runtime/composables/useDicomGuard.js';
|
|
10
|
+
export { StackCineOptions } from '../dist/runtime/composables/useStackCine.js';
|
|
11
|
+
export { AnnotationReportOptions } from '../dist/runtime/composables/useAnnotationReport.js';
|
|
12
|
+
export { ToolShortcut, ViewerShortcutHandlers, ViewerShortcutOptions } from '../dist/runtime/composables/useViewerShortcuts.js';
|
|
13
|
+
export { AddBoxesOptions, AddBoxesResult, AddJsonOptions, AddJsonResult, DicomBox, PixelBox } from '../dist/runtime/composables/useDicomAnnotations.js';
|
|
14
|
+
export { Measurement, MeasurementsOptions } from '../dist/runtime/composables/useMeasurements.js';
|
|
15
|
+
export { AnnotationFormat, FindingLabelInfo, ReadAnnotationsOptions, ReadAnnotationsResult } from '../dist/runtime/annotation-json.js';
|
|
16
|
+
|
|
17
|
+
type ModuleOptions = CornerstoneModuleOptions;
|
|
18
|
+
|
|
19
|
+
declare const _default: _nuxt_schema.NuxtModule<CornerstoneModuleOptions, CornerstoneModuleOptions, false>;
|
|
20
|
+
|
|
21
|
+
export { _default as default };
|
|
22
|
+
export type { ModuleOptions };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { resolve, dirname } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { defineNuxtModule, useLogger, createResolver, addPlugin, addImportsDir, addComponent, tryResolveModule } from '@nuxt/kit';
|
|
6
|
+
import { defu } from 'defu';
|
|
7
|
+
import { DEFAULT_TOOLS } from '../dist/runtime/types.js';
|
|
8
|
+
|
|
9
|
+
const MODULE_NAME = "nuxt-cornerstone";
|
|
10
|
+
const REQUIRED_PACKAGES = [
|
|
11
|
+
"@cornerstonejs/core",
|
|
12
|
+
"@cornerstonejs/tools",
|
|
13
|
+
"@cornerstonejs/dicom-image-loader",
|
|
14
|
+
"@cornerstonejs/metadata",
|
|
15
|
+
"@cornerstonejs/utils",
|
|
16
|
+
"dicom-parser"
|
|
17
|
+
];
|
|
18
|
+
const BROWSER_ONLY_PACKAGES = [
|
|
19
|
+
"@cornerstonejs/core",
|
|
20
|
+
"@cornerstonejs/tools",
|
|
21
|
+
"@cornerstonejs/dicom-image-loader"
|
|
22
|
+
];
|
|
23
|
+
const module$1 = defineNuxtModule({
|
|
24
|
+
meta: {
|
|
25
|
+
name: MODULE_NAME,
|
|
26
|
+
configKey: "cornerstone",
|
|
27
|
+
compatibility: { nuxt: ">=4.0.0" }
|
|
28
|
+
},
|
|
29
|
+
defaults: {
|
|
30
|
+
autoInit: true,
|
|
31
|
+
core: {},
|
|
32
|
+
dicomImageLoader: {},
|
|
33
|
+
tools: { enabled: true, register: [...DEFAULT_TOOLS] },
|
|
34
|
+
i18n: {
|
|
35
|
+
locale: "en",
|
|
36
|
+
fallbackLocale: "en",
|
|
37
|
+
messages: {},
|
|
38
|
+
numberingSystem: "auto",
|
|
39
|
+
detect: false
|
|
40
|
+
},
|
|
41
|
+
viteCommonjs: true,
|
|
42
|
+
prefix: "Cornerstone",
|
|
43
|
+
renderingEngineId: "nuxt-cornerstone",
|
|
44
|
+
toolGroupId: "nuxt-cornerstone-tools"
|
|
45
|
+
},
|
|
46
|
+
async setup(options, nuxt) {
|
|
47
|
+
const logger = useLogger(MODULE_NAME);
|
|
48
|
+
const { resolve } = createResolver(import.meta.url);
|
|
49
|
+
await assertPeerDependencies(nuxt.options.rootDir, logger);
|
|
50
|
+
const { value: publicOptions, dropped } = stripFunctions(options);
|
|
51
|
+
if (dropped.length > 0) {
|
|
52
|
+
logger.warn(
|
|
53
|
+
`These options are functions and cannot be serialized into runtimeConfig: ${dropped.join(", ")}. Set \`cornerstone.autoInit: false\` and pass them to \`ensureCornerstone()\` from your own plugin instead.`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const runtimeConfig = nuxt.options.runtimeConfig.public;
|
|
57
|
+
runtimeConfig.cornerstone = defu(
|
|
58
|
+
runtimeConfig.cornerstone,
|
|
59
|
+
publicOptions
|
|
60
|
+
);
|
|
61
|
+
nuxt.hook("vite:extendConfig", async (viteConfig, { isClient }) => {
|
|
62
|
+
if (!isClient) return;
|
|
63
|
+
const config = viteConfig;
|
|
64
|
+
config.optimizeDeps ??= {};
|
|
65
|
+
config.optimizeDeps.exclude = unique([
|
|
66
|
+
...config.optimizeDeps.exclude ?? [],
|
|
67
|
+
"@cornerstonejs/dicom-image-loader"
|
|
68
|
+
]);
|
|
69
|
+
config.optimizeDeps.include = unique([
|
|
70
|
+
...config.optimizeDeps.include ?? [],
|
|
71
|
+
"dicom-parser",
|
|
72
|
+
"fflate",
|
|
73
|
+
"@cornerstonejs/core",
|
|
74
|
+
"@cornerstonejs/tools",
|
|
75
|
+
"@cornerstonejs/metadata",
|
|
76
|
+
"@cornerstonejs/utils"
|
|
77
|
+
]);
|
|
78
|
+
config.worker = { ...config.worker, format: "es" };
|
|
79
|
+
config.assetsInclude = unique([...toArray(config.assetsInclude), "**/*.wasm"]);
|
|
80
|
+
if (nuxt.options.dev) {
|
|
81
|
+
config.plugins ??= [];
|
|
82
|
+
config.plugins.push(stripDanglingSourcemaps());
|
|
83
|
+
}
|
|
84
|
+
if (options.viteCommonjs) {
|
|
85
|
+
const { viteCommonjs } = await import('@originjs/vite-plugin-commonjs');
|
|
86
|
+
config.plugins ??= [];
|
|
87
|
+
config.plugins.push(viteCommonjs());
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
nuxt.options.nitro.externals = defu(nuxt.options.nitro.externals, {
|
|
91
|
+
external: BROWSER_ONLY_PACKAGES
|
|
92
|
+
});
|
|
93
|
+
addPlugin({ src: resolve("./runtime/plugin.i18n") });
|
|
94
|
+
addPlugin({ src: resolve("./runtime/plugin.client"), mode: "client" });
|
|
95
|
+
addImportsDir(resolve("./runtime/composables"));
|
|
96
|
+
addComponent({
|
|
97
|
+
name: `${options.prefix}Viewport`,
|
|
98
|
+
filePath: resolve("./runtime/components/CornerstoneViewport.vue"),
|
|
99
|
+
// Cornerstone needs a DOM, a WebGL context and web workers. Client-only
|
|
100
|
+
// registration means consumers do not have to remember <ClientOnly>.
|
|
101
|
+
mode: "client"
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
const MAP_COMMENT = /\/\/[#@]\s*sourceMappingURL=(\S+)\s*$/;
|
|
106
|
+
function stripDanglingSourcemaps() {
|
|
107
|
+
return {
|
|
108
|
+
name: "nuxt-cornerstone:dangling-sourcemaps",
|
|
109
|
+
enforce: "pre",
|
|
110
|
+
apply: "serve",
|
|
111
|
+
async load(id) {
|
|
112
|
+
const file = id.split("?")[0];
|
|
113
|
+
if (!file.endsWith(".js") || !file.includes("node_modules")) return null;
|
|
114
|
+
let code;
|
|
115
|
+
try {
|
|
116
|
+
code = await readFile(file, "utf8");
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const target = code.match(MAP_COMMENT)?.[1];
|
|
121
|
+
if (!target || target.startsWith("data:")) return null;
|
|
122
|
+
if (existsSync(resolve(dirname(file), target))) return null;
|
|
123
|
+
return { code: code.replace(MAP_COMMENT, ""), map: null };
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async function assertPeerDependencies(rootDir, logger) {
|
|
128
|
+
const from = [pathToFileURL(rootDir + "/").href, import.meta.url];
|
|
129
|
+
const missing = [];
|
|
130
|
+
for (const name of REQUIRED_PACKAGES) {
|
|
131
|
+
const resolved = await tryResolveModule(`${name}/package.json`, from);
|
|
132
|
+
if (!resolved) missing.push(name);
|
|
133
|
+
}
|
|
134
|
+
if (missing.length === 0) return;
|
|
135
|
+
const install = missing.map((name) => `${name}@^5.10.7`).join(" ");
|
|
136
|
+
logger.error(`Missing peer dependencies: ${missing.join(", ")}`);
|
|
137
|
+
throw new Error(
|
|
138
|
+
`[${MODULE_NAME}] missing peer dependencies: ${missing.join(", ")}.
|
|
139
|
+
Install them with:
|
|
140
|
+
npm install ${install}
|
|
141
|
+
All @cornerstonejs packages must be on the same version \u2014 they pin each other exactly.`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
function stripFunctions(value, path = []) {
|
|
145
|
+
if (typeof value === "function") {
|
|
146
|
+
return { value: void 0, dropped: [path.join(".")] };
|
|
147
|
+
}
|
|
148
|
+
if (Array.isArray(value) || value === null || typeof value !== "object") {
|
|
149
|
+
return { value, dropped: [] };
|
|
150
|
+
}
|
|
151
|
+
const out = {};
|
|
152
|
+
const dropped = [];
|
|
153
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
154
|
+
const result = stripFunctions(entry, [...path, key]);
|
|
155
|
+
dropped.push(...result.dropped);
|
|
156
|
+
if (result.value !== void 0) out[key] = result.value;
|
|
157
|
+
}
|
|
158
|
+
return { value: out, dropped };
|
|
159
|
+
}
|
|
160
|
+
function toArray(value) {
|
|
161
|
+
if (value === void 0) return [];
|
|
162
|
+
return Array.isArray(value) ? value : [value];
|
|
163
|
+
}
|
|
164
|
+
function unique(values) {
|
|
165
|
+
return [...new Set(values)];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export { module$1 as default };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { DicomBox } from './composables/useDicomAnnotations.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reading a JSON report into the boxes {@link useDicomAnnotations} draws.
|
|
4
|
+
*
|
|
5
|
+
* Two shapes are accepted, because the two that matter in practice are not the
|
|
6
|
+
* same thing. One is this module's own: a flat list of boxes already keyed by
|
|
7
|
+
* SOPInstanceUID, which is what an application produces when it has done its
|
|
8
|
+
* own translation. The other is a detector's nested output — findings, each
|
|
9
|
+
* holding the per-slice boxes it was seen on — which is what arrives when the
|
|
10
|
+
* report comes straight off an inference service.
|
|
11
|
+
*
|
|
12
|
+
* Nothing here guesses at coordinates. A box is only read from fields that are
|
|
13
|
+
* unambiguously in image pixel space; a finding-level bounding box in the
|
|
14
|
+
* model's own resampled volume is ignored, since it does not describe the DICOM
|
|
15
|
+
* images on screen.
|
|
16
|
+
*/
|
|
17
|
+
/** Which of the accepted layouts a file turned out to be. */
|
|
18
|
+
export type AnnotationFormat = 'boxes' | 'findings';
|
|
19
|
+
export interface ReadAnnotationsOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Drop findings whose confidence is below this, 0..1. Only the nested
|
|
22
|
+
* detector shape carries confidence; a flat box list is unaffected.
|
|
23
|
+
* Default: `0` — keep everything the file lists.
|
|
24
|
+
*/
|
|
25
|
+
minConfidence?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Build the caption for a box read from the detector shape. Default:
|
|
28
|
+
* the finding's own `name`/`label`, followed by its confidence as a
|
|
29
|
+
* percentage when it has one.
|
|
30
|
+
*/
|
|
31
|
+
label?: (finding: FindingLabelInfo) => string | undefined;
|
|
32
|
+
}
|
|
33
|
+
/** What {@link ReadAnnotationsOptions.label} is given about a finding. */
|
|
34
|
+
export interface FindingLabelInfo {
|
|
35
|
+
/** 1-based position among the findings that survived the threshold. */
|
|
36
|
+
index: number;
|
|
37
|
+
/** The finding's own name or numeric class, when it carries one. */
|
|
38
|
+
name: string | null;
|
|
39
|
+
/** 0..1, or `null` when the report does not say. */
|
|
40
|
+
confidence: number | null;
|
|
41
|
+
/** The slice this particular box sits on. */
|
|
42
|
+
sopInstanceUid: string;
|
|
43
|
+
}
|
|
44
|
+
export interface ReadAnnotationsResult {
|
|
45
|
+
/** Boxes ready to hand to `addBoxes()`. */
|
|
46
|
+
boxes: DicomBox[];
|
|
47
|
+
/** Which layout the file used. */
|
|
48
|
+
format: AnnotationFormat;
|
|
49
|
+
/** Findings read, before the confidence threshold. `0` for a flat list. */
|
|
50
|
+
findings: number;
|
|
51
|
+
/** Findings dropped by {@link ReadAnnotationsOptions.minConfidence}. */
|
|
52
|
+
filtered: number;
|
|
53
|
+
/**
|
|
54
|
+
* Entries that were the right shape's wrong shape — a box missing its UID,
|
|
55
|
+
* a coordinate that is not a number. They are counted rather than thrown on,
|
|
56
|
+
* so one bad row does not discard a usable report.
|
|
57
|
+
*/
|
|
58
|
+
malformed: number;
|
|
59
|
+
/**
|
|
60
|
+
* SeriesInstanceUID the report names, when it names one. Worth showing when
|
|
61
|
+
* no box matches anything loaded: it says which study to open.
|
|
62
|
+
*/
|
|
63
|
+
seriesInstanceUid: string | null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Read a JSON annotation file.
|
|
67
|
+
*
|
|
68
|
+
* Takes what a file picker, a drop target or `$fetch` hands over: a `File` or
|
|
69
|
+
* `Blob`, the JSON text, or the already-parsed object. Throws when the bytes
|
|
70
|
+
* are not JSON or the JSON is not one of the accepted layouts; a file that is
|
|
71
|
+
* simply empty of boxes comes back with an empty list instead, since a report
|
|
72
|
+
* finding nothing is a result, not a failure.
|
|
73
|
+
*/
|
|
74
|
+
export declare function readAnnotationJson(input: File | Blob | string | unknown, options?: ReadAnnotationsOptions): Promise<ReadAnnotationsResult>;
|
|
75
|
+
/** The synchronous half, for callers that already hold the parsed value. */
|
|
76
|
+
export declare function parseAnnotationJson(value: unknown, options?: ReadAnnotationsOptions): ReadAnnotationsResult;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { t } from "./i18n/index.js";
|
|
2
|
+
export async function readAnnotationJson(input, options = {}) {
|
|
3
|
+
return parseAnnotationJson(await toJson(input), options);
|
|
4
|
+
}
|
|
5
|
+
async function toJson(input) {
|
|
6
|
+
const text = typeof input === "string" ? input : input instanceof Blob ? await input.text() : null;
|
|
7
|
+
if (text === null) return input;
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(text);
|
|
10
|
+
} catch (caught) {
|
|
11
|
+
const message = caught instanceof Error ? caught.message : String(caught);
|
|
12
|
+
throw new Error(t("annotationJson.notJson", { message }), { cause: caught });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function parseAnnotationJson(value, options = {}) {
|
|
16
|
+
const boxList = findBoxList(value);
|
|
17
|
+
if (boxList) return readBoxList(boxList, value);
|
|
18
|
+
const findings = findFindingList(value);
|
|
19
|
+
if (findings) return readFindings(findings, value, options);
|
|
20
|
+
throw new Error(t("annotationJson.unknownShape"));
|
|
21
|
+
}
|
|
22
|
+
function isRecord(value) {
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
function arrayAt(value, keys) {
|
|
26
|
+
if (!isRecord(value)) return null;
|
|
27
|
+
for (const key of keys) {
|
|
28
|
+
const found = value[key];
|
|
29
|
+
if (Array.isArray(found)) return found;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function findBoxList(value) {
|
|
34
|
+
if (Array.isArray(value)) return looksLikeBox(value[0]) ? value : null;
|
|
35
|
+
const found = arrayAt(value, ["boxes", "annotations"]);
|
|
36
|
+
return found && looksLikeBox(found[0]) ? found : null;
|
|
37
|
+
}
|
|
38
|
+
function looksLikeBox(entry) {
|
|
39
|
+
if (!isRecord(entry)) return false;
|
|
40
|
+
return "box" in entry || "sopInstanceUid" in entry || "sop_instance_uid" in entry;
|
|
41
|
+
}
|
|
42
|
+
function findFindingList(value) {
|
|
43
|
+
const nested = isRecord(value) ? value.predictions : null;
|
|
44
|
+
return arrayAt(nested, ["findings"]) ?? arrayAt(value, ["findings"]);
|
|
45
|
+
}
|
|
46
|
+
function readBoxList(entries, document) {
|
|
47
|
+
const boxes = [];
|
|
48
|
+
let malformed = 0;
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
const box = toBox(entry);
|
|
51
|
+
if (box) boxes.push(box);
|
|
52
|
+
else malformed += 1;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
boxes,
|
|
56
|
+
format: "boxes",
|
|
57
|
+
findings: 0,
|
|
58
|
+
filtered: 0,
|
|
59
|
+
malformed,
|
|
60
|
+
seriesInstanceUid: seriesUidOf(document)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function toBox(entry) {
|
|
64
|
+
if (!isRecord(entry)) return null;
|
|
65
|
+
const sopInstanceUid = textOf(entry.sopInstanceUid ?? entry.sop_instance_uid);
|
|
66
|
+
if (!sopInstanceUid) return null;
|
|
67
|
+
const corners = toCorners(entry.box ?? entry.bounding_box ?? entry.boundingBox ?? entry);
|
|
68
|
+
if (!corners) return null;
|
|
69
|
+
const label = textOf(entry.label ?? entry.text ?? entry.name);
|
|
70
|
+
const uid = textOf(entry.uid ?? entry.annotationUID ?? entry.id);
|
|
71
|
+
return { sopInstanceUid, box: corners, ...label ? { label } : {}, ...uid ? { uid } : {} };
|
|
72
|
+
}
|
|
73
|
+
function toCorners(value) {
|
|
74
|
+
if (Array.isArray(value) && value.length === 4) {
|
|
75
|
+
const [x12 = Number.NaN, y12 = Number.NaN, x22 = Number.NaN, y22 = Number.NaN] = value.map(numberOf);
|
|
76
|
+
return finite(x12, y12, x22, y22) ? { x1: x12, y1: y12, x2: x22, y2: y22 } : null;
|
|
77
|
+
}
|
|
78
|
+
if (!isRecord(value)) return null;
|
|
79
|
+
const x1 = numberOf(value.x1 ?? value.upper_left_x ?? value.upperLeftX ?? value.xMin ?? value.x);
|
|
80
|
+
const y1 = numberOf(value.y1 ?? value.upper_left_y ?? value.upperLeftY ?? value.yMin ?? value.y);
|
|
81
|
+
if (!finite(x1, y1)) return null;
|
|
82
|
+
const x2 = numberOf(value.x2 ?? value.lower_right_x ?? value.lowerRightX ?? value.xMax);
|
|
83
|
+
const y2 = numberOf(value.y2 ?? value.lower_right_y ?? value.lowerRightY ?? value.yMax);
|
|
84
|
+
if (finite(x2, y2)) return { x1, y1, x2, y2 };
|
|
85
|
+
const width = numberOf(value.width ?? value.w);
|
|
86
|
+
const height = numberOf(value.height ?? value.h);
|
|
87
|
+
if (finite(width, height)) return { x1, y1, x2: x1 + width, y2: y1 + height };
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
function readFindings(entries, document, options) {
|
|
91
|
+
const { minConfidence = 0, label = defaultLabel } = options;
|
|
92
|
+
const boxes = [];
|
|
93
|
+
let filtered = 0;
|
|
94
|
+
let malformed = 0;
|
|
95
|
+
let index = 0;
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
if (!isRecord(entry)) {
|
|
98
|
+
malformed += 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const confidence = numberOf(entry.confidence ?? entry.score ?? entry.probability);
|
|
102
|
+
const hasConfidence = Number.isFinite(confidence);
|
|
103
|
+
if (hasConfidence && confidence < minConfidence) {
|
|
104
|
+
filtered += 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const slices = arrayAt(entry, ["slice_findings", "sliceFindings", "slices", "boxes"]);
|
|
108
|
+
if (!slices) {
|
|
109
|
+
malformed += 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
index += 1;
|
|
113
|
+
const name = textOf(entry.name ?? entry.class_name ?? entry.className) ?? numberText(entry.label ?? entry.class ?? entry.category);
|
|
114
|
+
for (const slice of slices) {
|
|
115
|
+
const box = toBox(slice);
|
|
116
|
+
if (!box) {
|
|
117
|
+
malformed += 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
boxes.push({
|
|
121
|
+
...box,
|
|
122
|
+
// A slice that captioned itself keeps its caption; the rest are
|
|
123
|
+
// described by the finding they belong to.
|
|
124
|
+
label: box.label ?? label({
|
|
125
|
+
index,
|
|
126
|
+
name,
|
|
127
|
+
confidence: hasConfidence ? confidence : null,
|
|
128
|
+
sopInstanceUid: box.sopInstanceUid
|
|
129
|
+
}),
|
|
130
|
+
// A detector's boxes carry no id of their own, and adding the same
|
|
131
|
+
// report twice must replace its boxes rather than stack a second set
|
|
132
|
+
// on the first, so one is derived from what does identify a box: the
|
|
133
|
+
// finding it belongs to and the slice it sits on.
|
|
134
|
+
uid: box.uid ?? `finding-${index}-${box.sopInstanceUid}`
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
boxes,
|
|
140
|
+
format: "findings",
|
|
141
|
+
findings: entries.length,
|
|
142
|
+
filtered,
|
|
143
|
+
malformed,
|
|
144
|
+
seriesInstanceUid: seriesUidOf(document)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function defaultLabel(finding) {
|
|
148
|
+
const name = finding.name ?? t("annotationJson.finding", { index: finding.index });
|
|
149
|
+
if (finding.confidence === null) return name;
|
|
150
|
+
return t("annotationJson.labelled", {
|
|
151
|
+
name,
|
|
152
|
+
confidence: Math.round(finding.confidence * 100)
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function seriesUidOf(document) {
|
|
156
|
+
if (!isRecord(document)) return null;
|
|
157
|
+
const scope = isRecord(document.predictions) ? document.predictions : document;
|
|
158
|
+
return textOf(scope.seriesInstanceUid ?? scope.series_instance_uid);
|
|
159
|
+
}
|
|
160
|
+
function numberOf(value) {
|
|
161
|
+
if (typeof value === "number") return value;
|
|
162
|
+
if (typeof value === "string" && value.trim()) return Number(value);
|
|
163
|
+
return Number.NaN;
|
|
164
|
+
}
|
|
165
|
+
function finite(...values) {
|
|
166
|
+
return values.every((value) => Number.isFinite(value));
|
|
167
|
+
}
|
|
168
|
+
function textOf(value) {
|
|
169
|
+
if (typeof value !== "string") return null;
|
|
170
|
+
const trimmed = value.trim();
|
|
171
|
+
return trimmed ? trimmed : null;
|
|
172
|
+
}
|
|
173
|
+
function numberText(value) {
|
|
174
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
175
|
+
return t("annotationJson.class", { value });
|
|
176
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Cornerstone3D stack viewport.
|
|
3
|
+
*
|
|
4
|
+
* Registered with `mode: 'client'`, so Nuxt never renders it on the server —
|
|
5
|
+
* WebGL, web workers and the WASM decoders have no server-side equivalent.
|
|
6
|
+
*/
|
|
7
|
+
type __VLS_Props = {
|
|
8
|
+
/** `wadouri:` / `wadors:` / `dicomfile:` imageIds, in display order. */
|
|
9
|
+
imageIds: string[];
|
|
10
|
+
/** Defaults to a generated id, unique per component instance. */
|
|
11
|
+
viewportId?: string;
|
|
12
|
+
renderingEngineId?: string;
|
|
13
|
+
toolGroupId?: string;
|
|
14
|
+
/** Index into `imageIds`. Two-way via the `imageIndexChange` event. */
|
|
15
|
+
imageIndex?: number;
|
|
16
|
+
/** Canvas background as RGB in 0..1. */
|
|
17
|
+
background?: [number, number, number];
|
|
18
|
+
/** Tool to bind to left-drag once mounted, or `false` to leave it alone. */
|
|
19
|
+
defaultTool?: string | false;
|
|
20
|
+
};
|
|
21
|
+
declare var __VLS_1: {
|
|
22
|
+
status: "ready" | "error" | "loading";
|
|
23
|
+
error: Error | null;
|
|
24
|
+
viewport: import("@cornerstonejs/core").StackViewport | null;
|
|
25
|
+
};
|
|
26
|
+
type __VLS_Slots = {} & {
|
|
27
|
+
default?: (props: typeof __VLS_1) => any;
|
|
28
|
+
};
|
|
29
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
|
|
30
|
+
/** The Cornerstone stack viewport, once mounted. */
|
|
31
|
+
viewport: import("vue").ShallowRef<import("@cornerstonejs/core").StackViewport | null, import("@cornerstonejs/core").StackViewport | null>;
|
|
32
|
+
viewportId: string;
|
|
33
|
+
renderingEngineId: string;
|
|
34
|
+
status: import("vue").Ref<"ready" | "error" | "loading", "ready" | "error" | "loading">;
|
|
35
|
+
error: import("vue").ShallowRef<Error | null, Error | null>;
|
|
36
|
+
setImageIndex: (index: number) => Promise<void>;
|
|
37
|
+
resetCamera: () => void;
|
|
38
|
+
getRenderingEngine: () => import("@cornerstonejs/core").RenderingEngine | null;
|
|
39
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
40
|
+
ready: (viewport: import("@cornerstonejs/core").StackViewport) => any;
|
|
41
|
+
error: (error: Error) => any;
|
|
42
|
+
imageRendered: () => any;
|
|
43
|
+
imageIndexChange: (index: number) => any;
|
|
44
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
45
|
+
onReady?: ((viewport: import("@cornerstonejs/core").StackViewport) => any) | undefined;
|
|
46
|
+
onError?: ((error: Error) => any) | undefined;
|
|
47
|
+
onImageRendered?: (() => any) | undefined;
|
|
48
|
+
onImageIndexChange?: ((index: number) => any) | undefined;
|
|
49
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
50
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
51
|
+
declare const _default: typeof __VLS_export;
|
|
52
|
+
export default _default;
|
|
53
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
54
|
+
new (): {
|
|
55
|
+
$slots: S;
|
|
56
|
+
};
|
|
57
|
+
};
|