volview 4.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/.browserslistrc +4 -0
- package/.editorconfig +6 -0
- package/.env +6 -0
- package/.eslintrc.js +109 -0
- package/.gitattributes +1 -0
- package/.github/workflows/checks.yml +16 -0
- package/.github/workflows/publish.yml +52 -0
- package/.prettierignore +5 -0
- package/CONTRIBUTING.md +29 -0
- package/LICENSE +202 -0
- package/README.md +44 -0
- package/babel.config.js +3 -0
- package/documentation/config.js +15 -0
- package/documentation/content/KWVolViewLogo.png +0 -0
- package/documentation/content/KWVolViewLogo.svg +35 -0
- package/documentation/content/VolViewLogo.png +0 -0
- package/documentation/content/VolViewLogo.svg +22 -0
- package/documentation/content/doc/index.md +64 -0
- package/documentation/content/doc/loading_data.md +30 -0
- package/documentation/content/doc/mouse_controls.md +34 -0
- package/documentation/content/doc/quick_start_guide.md +75 -0
- package/documentation/content/doc/rendering.md +27 -0
- package/documentation/content/doc/state_files.md +18 -0
- package/documentation/content/doc/toolbar.md +20 -0
- package/documentation/content/doc/welcome_screen.md +23 -0
- package/documentation/content/gallery/01-volview-welcome-notes.jpg +0 -0
- package/documentation/content/gallery/01-volview-welcome.jpg +0 -0
- package/documentation/content/gallery/02-volview-about.jpg +0 -0
- package/documentation/content/gallery/02-volview-notifications.jpg +0 -0
- package/documentation/content/gallery/02-volview-settings.jpg +0 -0
- package/documentation/content/gallery/03-volview-sample-datasets.jpg +0 -0
- package/documentation/content/gallery/04-volview-active-data.jpg +0 -0
- package/documentation/content/gallery/05-volview-select-data-to-delete.jpg +0 -0
- package/documentation/content/gallery/06-volview-corner-annotation.jpg +0 -0
- package/documentation/content/gallery/07-volview-layout-notes.jpg +0 -0
- package/documentation/content/gallery/07-volview-layout.jpg +0 -0
- package/documentation/content/gallery/09-volview-layout-3DPrimary.jpg +0 -0
- package/documentation/content/gallery/10-volview-wl-pan-zoom-notes.jpg +0 -0
- package/documentation/content/gallery/10-volview-wl-pan-zoom.jpg +0 -0
- package/documentation/content/gallery/11-volview-paint-notes.jpg +0 -0
- package/documentation/content/gallery/11-volview-paint.jpg +0 -0
- package/documentation/content/gallery/12-volview-paint-options.jpg +0 -0
- package/documentation/content/gallery/13-volview-crop-notes.jpg +0 -0
- package/documentation/content/gallery/13-volview-crop.jpg +0 -0
- package/documentation/content/gallery/14-volview-ruler.jpg +0 -0
- package/documentation/content/gallery/15-volview-crosshairs.jpg +0 -0
- package/documentation/content/gallery/16-volview-rendering.jpg +0 -0
- package/documentation/content/gallery/17-volview-colormap-notes.jpg +0 -0
- package/documentation/content/gallery/17-volview-colormap.jpg +0 -0
- package/documentation/content/gallery/17-volview-opacity-notes.jpg +0 -0
- package/documentation/content/gallery/17-volview-opacity.jpg +0 -0
- package/documentation/content/gallery/17-volview-opacity2.jpg +0 -0
- package/documentation/content/gallery/18-volview-presets.jpg +0 -0
- package/documentation/content/gallery/19-volview-color.jpg +0 -0
- package/documentation/content/gallery/19-volview-color2.jpg +0 -0
- package/documentation/content/gallery/20-volview-lightfollowcamera0.jpg +0 -0
- package/documentation/content/gallery/20-volview-lightfollowcamera1.jpg +0 -0
- package/documentation/content/gallery/20-volview-lightfollowcamera2.jpg +0 -0
- package/documentation/content/gallery/20-volview-lightfollowcamera3.jpg +0 -0
- package/documentation/content/gallery/21-volview-hybrid-final.jpg +0 -0
- package/documentation/content/gallery/21-volview-hybrid0.0.jpg +0 -0
- package/documentation/content/gallery/21-volview-hybrid0.5.jpg +0 -0
- package/documentation/content/gallery/21-volview-hybrid1.0.jpg +0 -0
- package/documentation/content/gallery/LocalAmbientOcclusion.jpg +0 -0
- package/documentation/content/gallery/Notes.pptx +0 -0
- package/documentation/content/gallery/VolView-Overview.jpg +0 -0
- package/documentation/content/gallery/index.md +53 -0
- package/documentation/content/icon/favicon-16x16.png +0 -0
- package/documentation/content/icon/favicon-196x196.png +0 -0
- package/documentation/content/icon/favicon-32x32.png +0 -0
- package/documentation/content/icon/favicon-96x96.png +0 -0
- package/documentation/content/index.pug +43 -0
- package/documentation/content/logo.png +0 -0
- package/documentation/content/logo.svg +20 -0
- package/documentation/data/menu.yml +3 -0
- package/documentation/themes/navy/layout/partial/footer.swig +22 -0
- package/documentation/tpl/__en__ +31 -0
- package/documentation/tpl/__sidebar__ +11 -0
- package/package.json +96 -0
- package/patches/jsdom-global+3.0.2.patch +12 -0
- package/patches/vue-toastification+1.7.14.patch +41 -0
- package/patches/vuetify+2.6.10.patch +13 -0
- package/prettier.config.js +9 -0
- package/public/favicon.ico +0 -0
- package/public/global.css +20 -0
- package/public/index.html +28 -0
- package/public/pvm-sample.png +0 -0
- package/server/helper.py +147 -0
- package/server/host.js +134 -0
- package/server/serialize.js +111 -0
- package/server/serialize.py +203 -0
- package/server/server.py +84 -0
- package/server/test.py +17 -0
- package/src/assets/KitwareHeadAndNeck.jpg +0 -0
- package/src/assets/logo.png +0 -0
- package/src/assets/logo.svg +20 -0
- package/src/assets/samples/3DUS-Fetus.jpg +0 -0
- package/src/assets/samples/CTA-Head_and_Neck.jpg +0 -0
- package/src/assets/samples/MRA-Head_and_Neck.jpg +0 -0
- package/src/assets/samples/MRI-Cardiac.jpg +0 -0
- package/src/assets/samples/MRI-PROSTATEx.jpg +0 -0
- package/src/components/AboutBox.vue +157 -0
- package/src/components/AiModule.vue +60 -0
- package/src/components/AnnotationsModule.vue +37 -0
- package/src/components/App.vue +539 -0
- package/src/components/ColorFunctionSlider.vue +181 -0
- package/src/components/DataBrowser.vue +173 -0
- package/src/components/DragAndDrop.vue +107 -0
- package/src/components/GroupableItem.vue +30 -0
- package/src/components/ImageDataBrowser.vue +186 -0
- package/src/components/ImageListCard.vue +114 -0
- package/src/components/ItemGroup.vue +74 -0
- package/src/components/LabelmapList.vue +36 -0
- package/src/components/LayoutGrid.vue +96 -0
- package/src/components/MeasurementsList.vue +134 -0
- package/src/components/MessageCenter.vue +189 -0
- package/src/components/MessageItem.vue +83 -0
- package/src/components/MessageNotificationContent.vue +20 -0
- package/src/components/MessageNotifications.vue +84 -0
- package/src/components/ModelBrowser.vue +83 -0
- package/src/components/ModulePanel.vue +137 -0
- package/src/components/PaintControls.vue +174 -0
- package/src/components/PatientBrowser.vue +280 -0
- package/src/components/PatientStudyVolumeBrowser.vue +325 -0
- package/src/components/RenderingModule.vue +49 -0
- package/src/components/ResizableNavDrawer.vue +158 -0
- package/src/components/SampleDataBrowser.vue +198 -0
- package/src/components/SaveSession.vue +57 -0
- package/src/components/Settings.vue +35 -0
- package/src/components/SliceSlider.vue +182 -0
- package/src/components/SpleenModule.vue +101 -0
- package/src/components/ToolButton.vue +58 -0
- package/src/components/ToolStrip.vue +186 -0
- package/src/components/ViewOverlayGrid.vue +84 -0
- package/src/components/VolumePresets.vue +165 -0
- package/src/components/VolumeProperties.vue +171 -0
- package/src/components/VolumeRendering.vue +365 -0
- package/src/components/VtkThreeView.vue +512 -0
- package/src/components/VtkTwoView.vue +665 -0
- package/src/components/__tests__/ResizableNavDrawer.spec.js +32 -0
- package/src/components/__tests__/ToolButton.spec.js +46 -0
- package/src/components/icons/KitwareLogoIcon.vue +54 -0
- package/src/components/icons/VolViewFullLogo.vue +139 -0
- package/src/components/icons/VolViewLogo.vue +78 -0
- package/src/components/styles/utils.css +16 -0
- package/src/components/styles/vtk-view.css +116 -0
- package/src/components/tools/CropTool.vue +61 -0
- package/src/components/tools/CrosshairsTool.vue +93 -0
- package/src/components/tools/MouseManipulatorTool.vue +108 -0
- package/src/components/tools/PaintTool.vue +54 -0
- package/src/components/tools/PanTool.vue +35 -0
- package/src/components/tools/RulerTool.vue +202 -0
- package/src/components/tools/SliceScrollTool.vue +123 -0
- package/src/components/tools/WindowLevelTool.vue +191 -0
- package/src/components/tools/ZoomTool.vue +35 -0
- package/src/components/tools/crop/Crop2D.vue +367 -0
- package/src/components/tools/crop/Crop2DLineHandle.vue +103 -0
- package/src/components/tools/crop/Crop3D.vue +192 -0
- package/src/components/tools/crop/CropControls.vue +38 -0
- package/src/components/tools/crop/types.ts +12 -0
- package/src/components/tools/crosshairs/CrosshairSVG2D.vue +125 -0
- package/src/components/tools/crosshairs/CrosshairsWidget2D.vue +100 -0
- package/src/components/tools/paint/PaintWidget2D.vue +192 -0
- package/src/components/tools/ruler/RulerSVG2D.vue +181 -0
- package/src/components/tools/ruler/RulerWidget2D.vue +154 -0
- package/src/composables/__tests__/useDoubleRecord.spec.ts +57 -0
- package/src/composables/__tests__/useOrientationLabels.spec.ts +29 -0
- package/src/composables/isViewAnimating.ts +25 -0
- package/src/composables/manageVTKSubscription.ts +6 -0
- package/src/composables/onProxyManagerEvent.ts +50 -0
- package/src/composables/proxyManager.ts +13 -0
- package/src/composables/sync/__tests__/createPrimitiveSyncContext.spec.ts +41 -0
- package/src/composables/sync/__tests__/useWindowingSync.spec.ts +51 -0
- package/src/composables/sync/createPrimitiveSyncContext.ts +83 -0
- package/src/composables/sync/useWindowingSync.ts +48 -0
- package/src/composables/useAppLoadingNotifications.ts +73 -0
- package/src/composables/useCameraOrientation.ts +34 -0
- package/src/composables/useCurrentImage.ts +74 -0
- package/src/composables/useCurrentSlice.ts +43 -0
- package/src/composables/useDoubleRecord.ts +77 -0
- package/src/composables/useEventListener.ts +26 -0
- package/src/composables/useGlobalErrorHook.ts +18 -0
- package/src/composables/useMultiSelection.ts +29 -0
- package/src/composables/useOrientationLabels.ts +62 -0
- package/src/composables/usePanels.ts +42 -0
- package/src/composables/usePersistCameraConfig.ts +138 -0
- package/src/composables/useResizeObserver.ts +37 -0
- package/src/composables/useResizeToFit.ts +50 -0
- package/src/composables/useSceneBuilder.ts +96 -0
- package/src/composables/useToast.js +11 -0
- package/src/composables/useVTKCallback.ts +33 -0
- package/src/composables/useVTKWorldToDisplay.ts +141 -0
- package/src/composables/useViewProxy.ts +87 -0
- package/src/composables/useVolumeThumbnailing.ts +173 -0
- package/src/composables/useWebGLWatchdog.ts +32 -0
- package/src/composables/useWidgetManager.ts +41 -0
- package/src/config.ts +182 -0
- package/src/constants.ts +38 -0
- package/src/core/id.ts +18 -0
- package/src/core/provider.ts +31 -0
- package/src/core/proxies.ts +90 -0
- package/src/core/thumbnailers/__tests__/vtk-image.spec.ts +58 -0
- package/src/core/thumbnailers/index.ts +5 -0
- package/src/core/thumbnailers/volume-thumbnailer.ts +130 -0
- package/src/core/thumbnailers/vtk-image.ts +142 -0
- package/src/core/tools/__tests__/paint.spec.ts +101 -0
- package/src/core/tools/__tests__/ruler.spec.ts +73 -0
- package/src/core/tools/paint/brush.ts +20 -0
- package/src/core/tools/paint/ellipse-brush.ts +96 -0
- package/src/core/tools/paint/index.ts +141 -0
- package/src/core/tools/ruler.ts +149 -0
- package/src/global.d.ts +24 -0
- package/src/io/__tests__/io.spec.ts +26 -0
- package/src/io/dicom.ts +187 -0
- package/src/io/index.ts +11 -0
- package/src/io/io.ts +157 -0
- package/src/io/itk/itkConfig.js +8 -0
- package/src/io/itk-dicom/dicom.cpp +327 -0
- package/src/io/itk-dicom/web-build/dicom.js +16 -0
- package/src/io/itk-dicom/web-build/dicom.umd.js +21 -0
- package/src/io/itk-dicom/web-build/dicom.umd.wasm +0 -0
- package/src/io/itk-dicom/web-build/dicom.wasm +0 -0
- package/src/io/manifest.ts +30 -0
- package/src/io/readers.ts +36 -0
- package/src/io/state-file/index.ts +170 -0
- package/src/io/state-file/schema.ts +277 -0
- package/src/io/state-file/utils.ts +33 -0
- package/src/io/types.ts +4 -0
- package/src/io/vtk/async.reader.worker.ts +50 -0
- package/src/io/vtk/async.ts +58 -0
- package/src/io/vtk/async.writer.worker.ts +46 -0
- package/src/io/vtk/common.ts +30 -0
- package/src/io/zip.ts +31 -0
- package/src/main.js +66 -0
- package/src/main.ts +1 -0
- package/src/plugins/vuetify.js +25 -0
- package/src/shims-itk.d.ts +67 -0
- package/src/shims-misc.d.ts +4 -0
- package/src/shims-tsx.d.ts +13 -0
- package/src/shims-vtk.d.ts +1042 -0
- package/src/shims-vue.d.ts +5 -0
- package/src/store/__tests__/datasets.spec.ts +75 -0
- package/src/store/__tests__/messages.spec.ts +74 -0
- package/src/store/__tests__/rulers.spec.ts +77 -0
- package/src/store/__tests__/view-configs.spec.ts +37 -0
- package/src/store/datasets-dicom.ts +398 -0
- package/src/store/datasets-files.ts +35 -0
- package/src/store/datasets-images.ts +118 -0
- package/src/store/datasets-labelmaps.ts +136 -0
- package/src/store/datasets-models.ts +25 -0
- package/src/store/datasets.ts +341 -0
- package/src/store/messages.ts +144 -0
- package/src/store/tools/crop.ts +136 -0
- package/src/store/tools/crosshairs.ts +131 -0
- package/src/store/tools/index.ts +95 -0
- package/src/store/tools/paint.ts +191 -0
- package/src/store/tools/rulers.ts +305 -0
- package/src/store/tools/types.ts +54 -0
- package/src/store/view-configs/camera.ts +51 -0
- package/src/store/view-configs/common.ts +61 -0
- package/src/store/view-configs/slicing.ts +86 -0
- package/src/store/view-configs/types.ts +39 -0
- package/src/store/view-configs/volume-coloring.ts +193 -0
- package/src/store/view-configs/windowing.ts +69 -0
- package/src/store/view-configs.ts +78 -0
- package/src/store/views.ts +130 -0
- package/src/types/index.ts +16 -0
- package/src/types/layout.ts +10 -0
- package/src/types/lps.ts +40 -0
- package/src/types/views.ts +65 -0
- package/src/types/vtk-types.ts +29 -0
- package/src/utils/__tests__/lps.spec.ts +102 -0
- package/src/utils/color.ts +23 -0
- package/src/utils/errors.js +3 -0
- package/src/utils/fetch.ts +101 -0
- package/src/utils/hacks.ts +14 -0
- package/src/utils/index.ts +83 -0
- package/src/utils/lps.ts +129 -0
- package/src/utils/manipulators.ts +35 -0
- package/src/utils/promise-worker.ts +45 -0
- package/src/utils/vtk-helpers.ts +215 -0
- package/src/utils/workerHandler.js +42 -0
- package/src/vtk/ColorMaps.ts +72 -0
- package/src/vtk/CrosshairsWidget/behavior.ts +112 -0
- package/src/vtk/CrosshairsWidget/index.d.ts +22 -0
- package/src/vtk/CrosshairsWidget/index.js +73 -0
- package/src/vtk/CrosshairsWidget/state.ts +51 -0
- package/src/vtk/IJKSliceRepresentationProxy/index.d.ts +9 -0
- package/src/vtk/IJKSliceRepresentationProxy/index.js +59 -0
- package/src/vtk/LPSView2DProxy/index.d.ts +18 -0
- package/src/vtk/LPSView2DProxy/index.js +89 -0
- package/src/vtk/LPSView3DProxy/index.d.ts +16 -0
- package/src/vtk/LPSView3DProxy/index.js +78 -0
- package/src/vtk/LabelMap/index.d.ts +16 -0
- package/src/vtk/LabelMap/index.js +49 -0
- package/src/vtk/LabelMapSliceRepProxy/index.d.ts +6 -0
- package/src/vtk/LabelMapSliceRepProxy/index.js +104 -0
- package/src/vtk/MedicalColorPresets.json +1448 -0
- package/src/vtk/PaintBrushContextRepresentation/index.js +191 -0
- package/src/vtk/PaintWidget/behavior.ts +101 -0
- package/src/vtk/PaintWidget/index.d.ts +28 -0
- package/src/vtk/PaintWidget/index.js +56 -0
- package/src/vtk/PaintWidget/state.ts +41 -0
- package/src/vtk/PiecewiseWidget/index.js +203 -0
- package/src/vtk/RulerWidget/behavior.ts +185 -0
- package/src/vtk/RulerWidget/index.d.ts +25 -0
- package/src/vtk/RulerWidget/index.js +79 -0
- package/src/vtk/RulerWidget/state.ts +63 -0
- package/src/vtk/TreJsonConverter/index.js +150 -0
- package/src/vtk/proxy.js +66 -0
- package/src/vtk/proxyUtils.js +16 -0
- package/src/vtk/webvr-empty.js +1 -0
- package/tests/testUtils.js +12 -0
- package/tsconfig.json +46 -0
- package/vue.config.js +86 -0
package/src/io/io.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { extensionToImageIO } from 'itk-wasm';
|
|
2
|
+
import { extractFilesFromZip } from './zip';
|
|
3
|
+
import { FileEntry } from './types';
|
|
4
|
+
|
|
5
|
+
export const ARCHIVE_FILE_TYPES = new Set(['zip', 'application/zip']);
|
|
6
|
+
|
|
7
|
+
export const ITK_IMAGE_EXTENSIONS = Array.from(
|
|
8
|
+
new Set(Array.from(extensionToImageIO.keys()).map((ext) => ext.toLowerCase()))
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
// remove duplicates
|
|
12
|
+
export const FILE_TYPES = Array.from(
|
|
13
|
+
new Set(['vti', 'vtp', 'stl', 'dcm', 'zip', 'json', ...ITK_IMAGE_EXTENSIONS])
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* file magic database
|
|
18
|
+
* Used to handle certain cases where files have no extension
|
|
19
|
+
*/
|
|
20
|
+
const FILE_MAGIC_DB = [
|
|
21
|
+
{
|
|
22
|
+
type: 'dcm',
|
|
23
|
+
skip: 128,
|
|
24
|
+
header: Array.from('DICM').map((c) => c.charCodeAt(0)),
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// How much data to read when extracting file magic.
|
|
29
|
+
// This should be generous enough for most files.
|
|
30
|
+
const HEAD_CHUNK = 512;
|
|
31
|
+
|
|
32
|
+
function prefixEquals(target: Uint8Array, prefix: number[] | Uint8Array) {
|
|
33
|
+
if (prefix.length > target.length) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
for (let i = 0; i < prefix.length; i += 1) {
|
|
37
|
+
if (prefix[i] !== target[i]) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function getFileTypeFromMagic(file: File): Promise<string | null> {
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
const head = file.slice(0, HEAD_CHUNK);
|
|
47
|
+
const reader = new FileReader();
|
|
48
|
+
reader.onload = () => {
|
|
49
|
+
const chunk = new Uint8Array(reader.result! as ArrayBuffer);
|
|
50
|
+
for (let i = 0; i < FILE_MAGIC_DB.length; i += 1) {
|
|
51
|
+
const { type, header, skip = 0 } = FILE_MAGIC_DB[i];
|
|
52
|
+
if (prefixEquals(chunk.slice(skip), header)) {
|
|
53
|
+
resolve(type);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
resolve(null);
|
|
58
|
+
};
|
|
59
|
+
reader.readAsArrayBuffer(head);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Retypes a given File.
|
|
65
|
+
*
|
|
66
|
+
* Handy for when a file object has no type given.
|
|
67
|
+
* Type is inferred from extension or magic.
|
|
68
|
+
*
|
|
69
|
+
* If type is not supported, file.type will be "".
|
|
70
|
+
* @param file
|
|
71
|
+
*/
|
|
72
|
+
export async function retypeFile(file: File): Promise<File> {
|
|
73
|
+
let type: string | null;
|
|
74
|
+
|
|
75
|
+
type =
|
|
76
|
+
FILE_TYPES.find((ext) => file.name.toLowerCase().endsWith(`.${ext}`)) ??
|
|
77
|
+
null;
|
|
78
|
+
|
|
79
|
+
if (!type) {
|
|
80
|
+
type = await getFileTypeFromMagic(file);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!type) {
|
|
84
|
+
type = '';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const retypedFile = new File([file], file.name, { type: type.toLowerCase() });
|
|
88
|
+
return retypedFile;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function extractArchivesRecursively(
|
|
92
|
+
files: File[]
|
|
93
|
+
): Promise<FileEntry[]> {
|
|
94
|
+
const results = await Promise.all(
|
|
95
|
+
files.map(async (file) => {
|
|
96
|
+
if (ARCHIVE_FILE_TYPES.has(file.type)) {
|
|
97
|
+
return extractFilesFromZip(file);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return [{ file, path: '' }];
|
|
101
|
+
})
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const archivedEntries = await Promise.all(
|
|
105
|
+
results.flat().map(async (entry) => {
|
|
106
|
+
return { file: await retypeFile(entry.file), path: entry.path };
|
|
107
|
+
})
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const archives = archivedEntries
|
|
111
|
+
.filter(({ file }) => ARCHIVE_FILE_TYPES.has(file.type))
|
|
112
|
+
.map(({ file }) => file);
|
|
113
|
+
|
|
114
|
+
if (archives.length > 0) {
|
|
115
|
+
const moreEntries = await extractArchivesRecursively(archives);
|
|
116
|
+
return [...archivedEntries, ...moreEntries];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return archivedEntries;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type ReadAsType = 'ArrayBuffer' | 'Text';
|
|
123
|
+
|
|
124
|
+
async function readFileAs<T extends ArrayBuffer | string>(
|
|
125
|
+
file: File,
|
|
126
|
+
type: ReadAsType
|
|
127
|
+
): Promise<T> {
|
|
128
|
+
return new Promise((resolve) => {
|
|
129
|
+
const fio = new globalThis.FileReader();
|
|
130
|
+
fio.onload = () => resolve(fio.result as T);
|
|
131
|
+
if (type === 'ArrayBuffer') {
|
|
132
|
+
fio.readAsArrayBuffer(file);
|
|
133
|
+
} else if (type === 'Text') {
|
|
134
|
+
fio.readAsText(file);
|
|
135
|
+
} else {
|
|
136
|
+
throw new TypeError(`readAs${type} is not a function`);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Reads a file and returns an ArrayBuffer
|
|
143
|
+
* @async
|
|
144
|
+
* @param {File} file
|
|
145
|
+
*/
|
|
146
|
+
export async function readFileAsArrayBuffer(file: File) {
|
|
147
|
+
return readFileAs<ArrayBuffer>(file, 'ArrayBuffer');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Reads a file and returns UTF-8 text
|
|
152
|
+
* @async
|
|
153
|
+
* @param {File} file
|
|
154
|
+
*/
|
|
155
|
+
export async function readFileAsUTF8Text(file: File) {
|
|
156
|
+
return readFileAs<string>(file, 'Text');
|
|
157
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
#include <cerrno>
|
|
2
|
+
#include <cstdio>
|
|
3
|
+
#include <dirent.h>
|
|
4
|
+
#include <fstream>
|
|
5
|
+
#include <iostream>
|
|
6
|
+
#include <stdexcept>
|
|
7
|
+
#include <string>
|
|
8
|
+
#include <sys/stat.h>
|
|
9
|
+
#include <sys/types.h>
|
|
10
|
+
#include <unordered_map>
|
|
11
|
+
#include <utility>
|
|
12
|
+
#include <vector>
|
|
13
|
+
|
|
14
|
+
#ifdef WEB_BUILD
|
|
15
|
+
// Building with the itk.js docker container has a more recent gcc version
|
|
16
|
+
#include <filesystem>
|
|
17
|
+
namespace fs = std::filesystem;
|
|
18
|
+
#else
|
|
19
|
+
// Building locally with gcc 7.5.0 means I need -lstdc++fs and
|
|
20
|
+
// experimental/filesystem
|
|
21
|
+
#include <experimental/filesystem>
|
|
22
|
+
namespace fs = std::experimental::filesystem;
|
|
23
|
+
#endif
|
|
24
|
+
|
|
25
|
+
#ifdef WEB_BUILD
|
|
26
|
+
#include <emscripten.h>
|
|
27
|
+
#endif
|
|
28
|
+
|
|
29
|
+
#include <fstream>
|
|
30
|
+
#include <iostream>
|
|
31
|
+
#include <nlohmann/json.hpp>
|
|
32
|
+
|
|
33
|
+
#include "itkCastImageFilter.h"
|
|
34
|
+
#include "itkGDCMImageIO.h"
|
|
35
|
+
#include "itkGDCMSeriesFileNames.h"
|
|
36
|
+
#include "itkImage.h"
|
|
37
|
+
#include "itkImageFileReader.h"
|
|
38
|
+
#include "itkImageIOBase.h"
|
|
39
|
+
#include "itkImageSeriesReader.h"
|
|
40
|
+
#include "itkRescaleIntensityImageFilter.h"
|
|
41
|
+
#include "itkVectorImage.h"
|
|
42
|
+
|
|
43
|
+
#include "itkOutputImage.h"
|
|
44
|
+
#include "itkOutputTextStream.h"
|
|
45
|
+
#include "itkPipeline.h"
|
|
46
|
+
|
|
47
|
+
#include "gdcmImageHelper.h"
|
|
48
|
+
#include "gdcmReader.h"
|
|
49
|
+
|
|
50
|
+
using json = nlohmann::json;
|
|
51
|
+
using ImageType = itk::Image<float, 3>;
|
|
52
|
+
using ReaderType = itk::ImageFileReader<ImageType>;
|
|
53
|
+
using SeriesReaderType = itk::ImageSeriesReader<ImageType>;
|
|
54
|
+
using FileNamesContainer = std::vector<std::string>;
|
|
55
|
+
using DicomIO = itk::GDCMImageIO;
|
|
56
|
+
// volumeID -> filenames[]
|
|
57
|
+
using VolumeMapType = std::unordered_map<std::string, std::vector<std::string>>;
|
|
58
|
+
// VolumeID[]
|
|
59
|
+
using VolumeIDList = std::vector<std::string>;
|
|
60
|
+
|
|
61
|
+
static const double EPSILON = 10e-5;
|
|
62
|
+
|
|
63
|
+
#ifdef WEB_BUILD
|
|
64
|
+
extern "C" const char *EMSCRIPTEN_KEEPALIVE unpack_error_what(intptr_t ptr) {
|
|
65
|
+
auto error = reinterpret_cast<std::runtime_error *>(ptr);
|
|
66
|
+
return error->what();
|
|
67
|
+
}
|
|
68
|
+
#endif
|
|
69
|
+
|
|
70
|
+
void replaceChars(std::string &str, char search, char replaceChar) {
|
|
71
|
+
int pos;
|
|
72
|
+
std::string replace(1, replaceChar);
|
|
73
|
+
while ((pos = str.find(search)) != std::string::npos) {
|
|
74
|
+
str.replace(pos, 1, replace);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// doesn't actually do any length checks, or overflow checks, or anything
|
|
79
|
+
// really.
|
|
80
|
+
template <int N>
|
|
81
|
+
double dotProduct(const std::vector<double> &vec1,
|
|
82
|
+
const std::vector<double> &vec2) {
|
|
83
|
+
double result = 0;
|
|
84
|
+
for (int i = 0; i < N; i++) {
|
|
85
|
+
result += vec1.at(i) * vec2.at(i);
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
std::vector<double> ReadImageOrientationValue(const std::string &filename) {
|
|
91
|
+
gdcm::Reader reader;
|
|
92
|
+
reader.SetFileName(filename.c_str());
|
|
93
|
+
if (!reader.Read()) {
|
|
94
|
+
throw std::runtime_error("gdcm: failed to read file");
|
|
95
|
+
}
|
|
96
|
+
const gdcm::File &file = reader.GetFile();
|
|
97
|
+
// This helper method asserts that the vector has length 6.
|
|
98
|
+
return gdcm::ImageHelper::GetDirectionCosinesValue(file);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
bool areCosinesAlmostEqual(std::vector<double> cosines1,
|
|
102
|
+
std::vector<double> cosines2,
|
|
103
|
+
double epsilon = EPSILON) {
|
|
104
|
+
for (int i = 0; i <= 1; i++) {
|
|
105
|
+
std::vector<double> vec1{cosines1.at(i), cosines1.at(i + 1),
|
|
106
|
+
cosines1.at(i + 2)};
|
|
107
|
+
std::vector<double> vec2{cosines2.at(i), cosines2.at(i + 1),
|
|
108
|
+
cosines2.at(i + 2)};
|
|
109
|
+
double dot = dotProduct<3>(vec1, vec2);
|
|
110
|
+
if (dot < (1 - EPSILON)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
VolumeMapType SeparateOnImageOrientation(const VolumeMapType &volumeMap) {
|
|
118
|
+
VolumeMapType newVolumeMap;
|
|
119
|
+
// Vector< Pair< cosines, volumeID >>
|
|
120
|
+
std::vector<std::pair<std::vector<double>, std::string>> cosinesToID;
|
|
121
|
+
|
|
122
|
+
// append unique ID part to the volume ID, based on cosines
|
|
123
|
+
// The format replaces non-alphanumeric chars to be semi-consistent with DICOM
|
|
124
|
+
// UID spec,
|
|
125
|
+
// and to make debugging easier when looking at the full volume IDs.
|
|
126
|
+
// Format: COSINE || "S" || COSINE || "S" || ...
|
|
127
|
+
// COSINE: A decimal number -DD.DDDD gets reformatted into NDDSDDDD
|
|
128
|
+
auto encodeCosinesAsIDPart = [](const std::vector<double> &cosines) {
|
|
129
|
+
std::string concatenated;
|
|
130
|
+
for (auto it = cosines.begin(); it != cosines.end(); ++it) {
|
|
131
|
+
concatenated += std::to_string(*it);
|
|
132
|
+
if (it != cosines.end() - 1) {
|
|
133
|
+
concatenated += 'S';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
replaceChars(concatenated, '-', 'N');
|
|
138
|
+
replaceChars(concatenated, '.', 'D');
|
|
139
|
+
|
|
140
|
+
return concatenated;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
for (const auto &[volumeID, names] : volumeMap) {
|
|
144
|
+
for (const auto &filename : names) {
|
|
145
|
+
std::vector<double> curCosines = ReadImageOrientationValue(filename);
|
|
146
|
+
|
|
147
|
+
bool inserted = false;
|
|
148
|
+
for (const auto &entry : cosinesToID) {
|
|
149
|
+
if (areCosinesAlmostEqual(curCosines, entry.first)) {
|
|
150
|
+
newVolumeMap[entry.second].push_back(filename);
|
|
151
|
+
inserted = true;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!inserted) {
|
|
157
|
+
const auto encodedIDPart = encodeCosinesAsIDPart(curCosines);
|
|
158
|
+
auto newID = volumeID + '.' + encodedIDPart;
|
|
159
|
+
newVolumeMap[newID].push_back(filename);
|
|
160
|
+
cosinesToID.push_back(std::make_pair(curCosines, newID));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return newVolumeMap;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* categorizeFiles extracts out the volumes contained within a set of DICOM
|
|
170
|
+
* files.
|
|
171
|
+
*
|
|
172
|
+
* Return a mapping of volume ID to the files containing the volume.
|
|
173
|
+
*/
|
|
174
|
+
int categorizeFiles(itk::wasm::Pipeline &pipeline) {
|
|
175
|
+
|
|
176
|
+
// inputs
|
|
177
|
+
FileNamesContainer files;
|
|
178
|
+
pipeline.add_option("-f,--files", files, "File names to categorize")
|
|
179
|
+
->required()
|
|
180
|
+
->check(CLI::ExistingFile)
|
|
181
|
+
->expected(1, -1);
|
|
182
|
+
|
|
183
|
+
// outputs
|
|
184
|
+
itk::wasm::OutputTextStream volumeMapJSONStream;
|
|
185
|
+
pipeline
|
|
186
|
+
.add_option("volumeMap", volumeMapJSONStream,
|
|
187
|
+
"JSON object encoding volumeID => filenames.")
|
|
188
|
+
->required();
|
|
189
|
+
|
|
190
|
+
ITK_WASM_PARSE(pipeline);
|
|
191
|
+
|
|
192
|
+
std::string path = "./";
|
|
193
|
+
|
|
194
|
+
// parse out series
|
|
195
|
+
typedef itk::GDCMSeriesFileNames SeriesFileNames;
|
|
196
|
+
SeriesFileNames::Pointer seriesFileNames = SeriesFileNames::New();
|
|
197
|
+
// files are all default dumped to cwd
|
|
198
|
+
seriesFileNames->SetDirectory(path);
|
|
199
|
+
seriesFileNames->SetUseSeriesDetails(true);
|
|
200
|
+
seriesFileNames->SetGlobalWarningDisplay(false);
|
|
201
|
+
seriesFileNames->AddSeriesRestriction("0008|0021");
|
|
202
|
+
seriesFileNames->SetRecursive(false);
|
|
203
|
+
// Does this affect series organization?
|
|
204
|
+
seriesFileNames->SetLoadPrivateTags(false);
|
|
205
|
+
|
|
206
|
+
// Obtain the initial separation of imported files into distinct volumes.
|
|
207
|
+
auto &gdcmSeriesUIDs = seriesFileNames->GetSeriesUIDs();
|
|
208
|
+
|
|
209
|
+
// The initial series UIDs are used as the basis for our volume IDs.
|
|
210
|
+
VolumeMapType volumeMap;
|
|
211
|
+
for (auto seriesUID : gdcmSeriesUIDs) {
|
|
212
|
+
volumeMap[seriesUID] = seriesFileNames->GetFileNames(seriesUID.c_str());
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// further restrict on orientation
|
|
216
|
+
volumeMap = SeparateOnImageOrientation(volumeMap);
|
|
217
|
+
|
|
218
|
+
// strip off tmp prefix
|
|
219
|
+
for (auto &entry : volumeMap) {
|
|
220
|
+
auto &fileNames = entry.second;
|
|
221
|
+
for (auto &f : fileNames) {
|
|
222
|
+
f = f.substr(path.size());
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Generate the JSON and add to output stream
|
|
227
|
+
auto volumeMapJSON = json(volumeMap);
|
|
228
|
+
volumeMapJSONStream.Get() << volumeMapJSON;
|
|
229
|
+
|
|
230
|
+
// Clean up files
|
|
231
|
+
for (auto &file : files) {
|
|
232
|
+
remove(file.c_str());
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return EXIT_SUCCESS;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Reads an image slice and returns the optionally thumbnailed image.
|
|
240
|
+
*/
|
|
241
|
+
int getSliceImage(itk::wasm::Pipeline &pipeline) {
|
|
242
|
+
|
|
243
|
+
// inputs
|
|
244
|
+
std::string fileName;
|
|
245
|
+
pipeline.add_option("-f,--file", fileName, "File name generate image for")
|
|
246
|
+
->required()
|
|
247
|
+
->check(CLI::ExistingFile)
|
|
248
|
+
->expected(1);
|
|
249
|
+
|
|
250
|
+
bool asThumbnail = false;
|
|
251
|
+
pipeline.add_option("-t,--thumbnail", asThumbnail,
|
|
252
|
+
"Generate thumbnail image");
|
|
253
|
+
|
|
254
|
+
ITK_WASM_PRE_PARSE(pipeline);
|
|
255
|
+
|
|
256
|
+
// Setup reader
|
|
257
|
+
typename DicomIO::Pointer dicomIO = DicomIO::New();
|
|
258
|
+
dicomIO->LoadPrivateTagsOff();
|
|
259
|
+
typename ReaderType::Pointer reader = ReaderType::New();
|
|
260
|
+
reader->SetFileName(fileName);
|
|
261
|
+
|
|
262
|
+
if (asThumbnail) {
|
|
263
|
+
using InputImageType = ImageType;
|
|
264
|
+
using OutputPixelType = uint8_t;
|
|
265
|
+
using OutputImageType = itk::Image<OutputPixelType, 3>;
|
|
266
|
+
using RescaleFilter =
|
|
267
|
+
itk::RescaleIntensityImageFilter<InputImageType, InputImageType>;
|
|
268
|
+
using CastImageFilter =
|
|
269
|
+
itk::CastImageFilter<InputImageType, OutputImageType>;
|
|
270
|
+
|
|
271
|
+
// outputs
|
|
272
|
+
using WasmOutputImageType = itk::wasm::OutputImage<OutputImageType>;
|
|
273
|
+
WasmOutputImageType outputImage;
|
|
274
|
+
pipeline.add_option("OutputImage", outputImage, "The slice")->required();
|
|
275
|
+
|
|
276
|
+
ITK_WASM_PARSE(pipeline);
|
|
277
|
+
|
|
278
|
+
auto rescaleFilter = RescaleFilter::New();
|
|
279
|
+
rescaleFilter->SetInput(reader->GetOutput());
|
|
280
|
+
rescaleFilter->SetOutputMinimum(0);
|
|
281
|
+
rescaleFilter->SetOutputMaximum(itk::NumericTraits<OutputPixelType>::max());
|
|
282
|
+
|
|
283
|
+
auto castFilter = CastImageFilter::New();
|
|
284
|
+
castFilter->SetInput(rescaleFilter->GetOutput());
|
|
285
|
+
castFilter->Update();
|
|
286
|
+
|
|
287
|
+
// Set the output image
|
|
288
|
+
outputImage.Set(castFilter->GetOutput());
|
|
289
|
+
} else {
|
|
290
|
+
// outputs
|
|
291
|
+
using WasmOutputImageType = itk::wasm::OutputImage<ImageType>;
|
|
292
|
+
WasmOutputImageType outputImage;
|
|
293
|
+
pipeline.add_option("OutputImage", outputImage, "The slice")->required();
|
|
294
|
+
|
|
295
|
+
ITK_WASM_PARSE(pipeline);
|
|
296
|
+
|
|
297
|
+
reader->Update();
|
|
298
|
+
outputImage.Set(reader->GetOutput());
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Clean up the file
|
|
302
|
+
remove(fileName.c_str());
|
|
303
|
+
|
|
304
|
+
return EXIT_SUCCESS;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
int main(int argc, char *argv[]) {
|
|
308
|
+
std::string action;
|
|
309
|
+
itk::wasm::Pipeline pipeline("VolView pipeline to access DICOM data", argc,
|
|
310
|
+
argv);
|
|
311
|
+
pipeline.add_option("-a,--action", action, "The action to run")
|
|
312
|
+
->check(CLI::IsMember({"categorize", "getSliceImage"}));
|
|
313
|
+
|
|
314
|
+
// Pre parse so we can get the action
|
|
315
|
+
ITK_WASM_PRE_PARSE(pipeline)
|
|
316
|
+
|
|
317
|
+
if (action == "categorize") {
|
|
318
|
+
|
|
319
|
+
ITK_WASM_CATCH_EXCEPTION(pipeline, categorizeFiles(pipeline));
|
|
320
|
+
|
|
321
|
+
} else if (action == "getSliceImage") {
|
|
322
|
+
|
|
323
|
+
ITK_WASM_CATCH_EXCEPTION(pipeline, getSliceImage(pipeline));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return EXIT_SUCCESS;
|
|
327
|
+
}
|