UNPKG

dcmjs-imaging

Version:

DICOM image and overlay rendering for Node.js and browser using dcmjs

519 lines (463 loc) 16.6 kB
<!doctype 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>