dcmjs-imaging
Version:
DICOM image and overlay rendering for Node.js and browser using dcmjs
519 lines (463 loc) • 16.6 kB
HTML
<html lang="en">
<head>
<title>dcmjs-imaging rendering example</title>
<meta charset="utf-8" />
<meta
name="viewport"
content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"
/>
<style>
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
display: table;
}
.container {
display: table-cell;
text-align: center;
vertical-align: middle;
}
.content {
display: inline-block;
text-align: center;
}
</style>
</head>
<body>
<div id="dropZone" class="container">
<div class="content">
<p id="infoText">
<a id="openLink" href="">Open</a> or drag and drop a DICOM Part 10 file to render it!<br />Nothing
gets uploaded anywhere.<br /><br />While holding the left mouse button, move the mouse
over the image to adjust window/level.<br />Use mouse wheel to scroll through multiple
frames.
</p>
<canvas id="renderingCanvas"></canvas>
</div>
</div>
</body>
<script type="text/javascript" src="https://unpkg.com/dcmjs"></script>
<script type="text/javascript" src="dcmjs-imaging.min.js"></script>
<script>
let webGpuAdapter;
let webGpuDevice;
let webGpuFormat;
const WebGPUVertexShaderCode = `
struct VertexOutput {
@builtin(position) position : vec4<f32>,
@location(0) texCoord : vec2<f32>,
};
@vertex
fn main_v(@location(0) position : vec2<f32>, @location(1) texCoord : vec2<f32>) -> VertexOutput {
var output : VertexOutput;
output.position = vec4<f32>(position, 0.0, 1.0);
output.texCoord = texCoord;
return output;
}
`;
const WebGPUFragmentShaderCode = `
@group(0) @binding(0) var texSampler: sampler;
@group(0) @binding(1) var tex: texture_2d<f32>;
@fragment
fn main_f(@location(0) texCoord : vec2<f32>) -> @location(0) vec4<f32> {
return textureSample(tex, texSampler, texCoord);
}
`;
const WebGLBaseVertexShader = `
attribute vec2 position;
varying vec2 texCoords;
void main() {
texCoords = (position + 1.0) / 2.0;
texCoords.y = 1.0 - texCoords.y;
gl_Position = vec4(position, 0, 1.0);
}
`;
const WebGLBaseFragmentShader = `
precision highp float;
varying vec2 texCoords;
uniform sampler2D textureSampler;
void main() {
vec4 color = texture2D(textureSampler, texCoords);
gl_FragColor = color;
}
`;
const { DicomImage, WindowLevel, NativePixelDecoder } = window.dcmjsImaging;
window.onload = async (event) => {
await NativePixelDecoder.initializeAsync();
if (navigator.gpu) {
webGpuAdapter = await navigator.gpu.requestAdapter();
webGpuDevice = await webGpuAdapter.requestDevice();
webGpuFormat = navigator.gpu.getPreferredCanvasFormat();
}
};
function renderFile(file) {
const reader = new FileReader();
reader.onload = (file) => {
const arrayBuffer = reader.result;
const canvasElement = document.getElementById('renderingCanvas');
canvasElement.onwheel = undefined;
canvasElement.onmousedown = undefined;
canvasElement.onmousemove = undefined;
canvasElement.onmouseup = undefined;
const infoTextElement = document.getElementById('infoText');
infoTextElement.innerText = '';
const t0 = performance.now();
let frame = 0;
let windowing = false;
let windowLevel = undefined;
let x = 0;
let y = 0;
let renderer = 'Canvas';
if (isWebGLAvailable()) {
renderer = 'WebGL';
} else if (isWebGpuAvailable()) {
renderer = 'WebGPU';
}
const image = new DicomImage(arrayBuffer);
const t1 = performance.now();
console.log(`Parsing time: ${t1 - t0} ms`);
console.log(`Width: ${image.getWidth()}`);
console.log(`Height: ${image.getHeight()}`);
console.log(`Number of frames: ${image.getNumberOfFrames()}`);
console.log(`Transfer syntax UID: ${image.getTransferSyntaxUid()}`);
canvasElement.onwheel = (event) => {
if (image.getNumberOfFrames() < 2) {
return;
}
event.preventDefault();
const next = event.deltaY > 0 ? 1 : -1;
if (frame + next > image.getNumberOfFrames() - 1 || frame + next < 0) {
return;
}
const renderingResult = renderFrame({
image,
frame: frame + next,
windowLevel,
canvasElement,
infoTextElement,
renderer,
});
frame = renderingResult.frame;
};
canvasElement.onmousedown = (event) => {
if (event.button !== 0 || !windowLevel) {
return;
}
x = event.offsetX;
y = event.offsetY;
windowing = true;
};
canvasElement.onmousemove = (event) => {
if (event.button !== 0 || !windowLevel) {
return;
}
if (windowing) {
const diffX = event.offsetX - x;
const diffY = event.offsetY - y;
x = event.offsetX;
y = event.offsetY;
const ww = windowLevel.getWindow();
const wl = windowLevel.getLevel();
if (ww + diffX <= 1) {
return;
}
windowLevel.setWindow(ww + diffX);
windowLevel.setLevel(wl + diffY);
const renderingResult = renderFrame({
image,
frame,
windowLevel,
canvasElement,
infoTextElement,
renderer,
});
windowLevel = renderingResult.windowLevel;
}
};
canvasElement.onmouseup = (event) => {
if (event.button !== 0 || !windowLevel) {
return;
}
x = 0;
y = 0;
windowing = false;
};
const renderingResult = renderFrame({
image,
frame,
windowLevel,
canvasElement,
infoTextElement,
renderer,
});
windowLevel = renderingResult.windowLevel;
};
if (file) {
reader.readAsArrayBuffer(file);
}
}
function renderFrame(opts) {
opts.infoTextElement.innerHTML = '';
opts.canvasElement.width = 0;
opts.canvasElement.height = 0;
try {
const t0 = performance.now();
const renderingResult = opts.image.render({
frame: opts.frame,
windowLevel: opts.windowLevel,
});
const t1 = performance.now();
opts.canvasElement.width = renderingResult.width;
opts.canvasElement.height = renderingResult.height;
if (opts.renderer === 'WebGPU') {
renderFrameWebGPU(renderingResult, opts);
} else if (opts.renderer === 'WebGL') {
renderFrameWebGL(renderingResult, opts);
} else {
renderFrameCanvas(renderingResult, opts);
}
const t2 = performance.now();
console.log(`Rendering frame: ${opts.frame}`);
if (renderingResult.windowLevel) {
console.log(`Rendering window: ${renderingResult.windowLevel.toString()}`);
}
console.log(`Rendering time: ${t1 - t0} ms`);
console.log(`Drawing time [${opts.renderer}]: ${t2 - t1} ms`);
return renderingResult;
} catch (err) {
opts.infoTextElement.innerText = 'Error: ' + err.message;
throw err;
}
}
function renderFrameWebGPU(renderingResult, opts) {
const renderedPixels = new Uint8ClampedArray(renderingResult.pixels);
const imageData = new ImageData(
renderedPixels,
renderingResult.width,
renderingResult.height
);
const context = opts.canvasElement.getContext('webgpu');
context.configure({
device: webGpuDevice,
format: webGpuFormat,
});
const shaderModule = webGpuDevice.createShaderModule({
code: WebGPUVertexShaderCode + WebGPUFragmentShaderCode,
});
const bindGroupLayout = webGpuDevice.createBindGroupLayout({
entries: [
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: {} },
],
});
const pipelineLayout = webGpuDevice.createPipelineLayout({
bindGroupLayouts: [bindGroupLayout],
});
const pipeline = webGpuDevice.createRenderPipeline({
layout: pipelineLayout,
vertex: {
module: shaderModule,
entryPoint: 'main_v',
buffers: [
{
arrayStride: 16,
attributes: [
{ shaderLocation: 0, offset: 0, format: 'float32x2' },
{ shaderLocation: 1, offset: 8, format: 'float32x2' },
],
},
],
},
fragment: {
module: shaderModule,
entryPoint: 'main_f',
targets: [
{
format: webGpuFormat,
},
],
},
primitive: {
topology: 'triangle-list',
},
});
// prettier-ignore
const vertexData = new Float32Array([
-1, -1, 0, 0,
1, -1, 1, 0,
1, 1, 1, 1,
1, 1, 1, 1,
-1, 1, 0, 1,
-1, -1, 0, 0
]);
const vertexBuffer = webGpuDevice.createBuffer({
size: vertexData.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
webGpuDevice.queue.writeBuffer(vertexBuffer, 0, vertexData);
const texture = webGpuDevice.createTexture({
size: [renderingResult.width, renderingResult.height, 1],
format: 'rgba8unorm',
usage:
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT,
});
webGpuDevice.queue.copyExternalImageToTexture(
{ source: imageData, flipY: true },
{ texture },
{ width: renderingResult.width, height: renderingResult.height }
);
const sampler = webGpuDevice.createSampler({
magFilter: 'linear',
minFilter: 'linear',
});
const bindGroup = webGpuDevice.createBindGroup({
layout: bindGroupLayout,
entries: [
{ binding: 0, resource: sampler },
{ binding: 1, resource: texture.createView() },
],
});
const textureView = context.getCurrentTexture().createView();
const renderPassDescriptor = {
colorAttachments: [
{
clearValue: { a: 1, b: 0, g: 0, r: 0 },
loadOp: 'clear',
storeOp: 'store',
view: textureView,
},
],
};
const commandEncoder = webGpuDevice.createCommandEncoder();
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
passEncoder.setVertexBuffer(0, vertexBuffer);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.draw(6);
passEncoder.end();
webGpuDevice.queue.submit([commandEncoder.finish()]);
}
function renderFrameWebGL(renderingResult, opts) {
const renderedPixels = new Uint8Array(renderingResult.pixels);
const gl = opts.canvasElement.getContext('webgl');
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
gl.clearColor(1.0, 1.0, 1.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, WebGLBaseVertexShader);
gl.compileShader(vertexShader);
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
throw new Error('Error compiling vertex shader', gl.getShaderInfoLog(vertexShader));
}
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, WebGLBaseFragmentShader);
gl.compileShader(fragmentShader);
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
throw new Error('Error compiling fragment shader', gl.getShaderInfoLog(fragmentShader));
}
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error('Error linking program', gl.getProgramInfoLog(program));
}
gl.validateProgram(program);
if (!gl.getProgramParameter(program, gl.VALIDATE_STATUS)) {
throw new Error('Error validating program', gl.getProgramInfoLog(program));
}
gl.useProgram(program);
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
// prettier-ignore
const vertices = new Float32Array([
-1, -1, 1,
-1, -1, 1,
1, -1, -1,
1, 1, 1
]);
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
const positionLocation = gl.getAttribLocation(program, 'position');
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(positionLocation);
const texture = gl.createTexture();
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
renderingResult.width,
renderingResult.height,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
renderedPixels
);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
function renderFrameCanvas(renderingResult, opts) {
const renderedPixels = new Uint8Array(renderingResult.pixels);
const ctx = opts.canvasElement.getContext('2d');
ctx.clearRect(0, 0, opts.canvasElement.width, opts.canvasElement.height);
const imageData = ctx.createImageData(renderingResult.width, renderingResult.height);
const canvasPixels = imageData.data;
for (let i = 0; i < 4 * renderingResult.width * renderingResult.height; i++) {
canvasPixels[i] = renderedPixels[i];
}
ctx.putImageData(imageData, 0, 0);
}
function isWebGLAvailable() {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
return gl instanceof WebGLRenderingContext;
}
function isWebGpuAvailable() {
if (!navigator.gpu) {
return false;
}
if (!webGpuAdapter || !webGpuDevice || !webGpuFormat) {
return false;
}
const canvas = document.createElement('canvas');
const gpu = canvas.getContext('webgpu');
return gpu instanceof GPUCanvasContext;
}
const dropZone = document.getElementById('dropZone');
dropZone.ondragover = (event) => {
event.stopPropagation();
event.preventDefault();
event.dataTransfer.dropEffect = 'copy';
};
dropZone.ondrop = (event) => {
event.stopPropagation();
event.preventDefault();
const files = event.dataTransfer.files;
renderFile(files[0]);
};
const openLink = document.getElementById('openLink');
openLink.onclick = () => {
const input = document.createElement('input');
input.type = 'file';
input.onchange = (event) => {
const files = event.target.files;
renderFile(files[0]);
};
input.click();
return false;
};
</script>
</html>