From 0f73c8f3c2cbd48ef4831da5f8380304e93c2ad2 Mon Sep 17 00:00:00 2001 From: 1532 Date: Wed, 24 Sep 2025 16:29:39 +0530 Subject: [PATCH 1/4] feat(image):Added image smoothing functionality for stack/volume viewport --- packages/core/examples/sharpening/index.ts | 2 +- .../src/RenderingEngine/BaseVolumeViewport.ts | 14 ++-- .../core/src/RenderingEngine/StackViewport.ts | 6 +- .../src/RenderingEngine/renderPasses/index.ts | 4 +- .../renderPasses/sharpeningRenderPass.ts | 65 +++++++++++++++---- 5 files changed, 68 insertions(+), 23 deletions(-) diff --git a/packages/core/examples/sharpening/index.ts b/packages/core/examples/sharpening/index.ts index e2424ce2e5..8c0adc4bfe 100644 --- a/packages/core/examples/sharpening/index.ts +++ b/packages/core/examples/sharpening/index.ts @@ -147,7 +147,7 @@ instructionsContainer.appendChild(instructionsList); addSliderToToolbar({ id: 'sharpening-slider', title: 'Sharpening', - range: [0, 300], + range: [-1000, 1000], defaultValue: 0, onSelectedValueChange: (value: number) => { // Get the rendering engine diff --git a/packages/core/src/RenderingEngine/BaseVolumeViewport.ts b/packages/core/src/RenderingEngine/BaseVolumeViewport.ts index fb28a4f6ae..950cb1b12c 100644 --- a/packages/core/src/RenderingEngine/BaseVolumeViewport.ts +++ b/packages/core/src/RenderingEngine/BaseVolumeViewport.ts @@ -77,7 +77,7 @@ import { isContextPoolRenderingEngine } from './helpers/isContextPoolRenderingEn import type vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer'; import mprCameraValues from '../constants/mprCameraValues'; import { isInvalidNumber } from './helpers/isInvalidNumber'; -import { createSharpeningRenderPass } from './renderPasses'; +import { createSharpeningAndSmoothingRenderPass } from './renderPasses'; /** * Abstract base class for volume viewports. VolumeViewports are used to render * 3D volumes from which various orientations can be viewed. Since VolumeViewports @@ -897,7 +897,11 @@ abstract class BaseVolumeViewport extends Viewport { [-viewPlaneNormal[0], -viewPlaneNormal[1], -viewPlaneNormal[2]], projectedDistance ); - const focalShift = vec3.subtract(vec3.create(), newImagePositionPatient, focalPoint); + const focalShift = vec3.subtract( + vec3.create(), + newImagePositionPatient, + focalPoint + ); const newPosition = vec3.add(vec3.create(), position, focalShift); // this.setViewReference({ // ...viewRef, @@ -905,7 +909,7 @@ abstract class BaseVolumeViewport extends Viewport { // }); this.setCamera({ focalPoint: newImagePositionPatient as Point3, - position: newPosition as Point3 + position: newPosition as Point3, }); this.render(); return; @@ -1070,7 +1074,7 @@ abstract class BaseVolumeViewport extends Viewport { * @returns True if custom render passes should be used, false otherwise */ protected shouldUseCustomRenderPass(): boolean { - return this.sharpening > 0 && !this.useCPURendering; + return !this.useCPURendering; } /** @@ -1084,7 +1088,7 @@ abstract class BaseVolumeViewport extends Viewport { } try { - return [createSharpeningRenderPass(this.sharpening)]; + return [createSharpeningAndSmoothingRenderPass(this.sharpening)]; } catch (e) { console.warn('Failed to create sharpening render passes:', e); return null; diff --git a/packages/core/src/RenderingEngine/StackViewport.ts b/packages/core/src/RenderingEngine/StackViewport.ts index 6392f56906..7c23947955 100644 --- a/packages/core/src/RenderingEngine/StackViewport.ts +++ b/packages/core/src/RenderingEngine/StackViewport.ts @@ -98,7 +98,7 @@ import getSpacingInNormalDirection from '../utilities/getSpacingInNormalDirectio import getClosestImageId from '../utilities/getClosestImageId'; import { adjustInitialViewUp } from '../utilities/adjustInitialViewUp'; import { isContextPoolRenderingEngine } from './helpers/isContextPoolRenderingEngine'; -import { createSharpeningRenderPass } from './renderPasses'; +import { createSharpeningAndSmoothingRenderPass } from './renderPasses'; export interface ImageDataMetaData { bitsAllocated: number; @@ -445,7 +445,7 @@ class StackViewport extends Viewport { * @returns True if custom render passes should be used, false otherwise */ protected shouldUseCustomRenderPass(): boolean { - return this.sharpening > 0 && !this.useCPURendering; + return !this.useCPURendering; } /** @@ -459,7 +459,7 @@ class StackViewport extends Viewport { } try { - return [createSharpeningRenderPass(this.sharpening)]; + return [createSharpeningAndSmoothingRenderPass(this.sharpening)]; } catch (e) { console.warn('Failed to create sharpening render passes:', e); return null; diff --git a/packages/core/src/RenderingEngine/renderPasses/index.ts b/packages/core/src/RenderingEngine/renderPasses/index.ts index 2bc95c12a0..723742eb7b 100644 --- a/packages/core/src/RenderingEngine/renderPasses/index.ts +++ b/packages/core/src/RenderingEngine/renderPasses/index.ts @@ -1,3 +1,3 @@ -import { createSharpeningRenderPass } from './sharpeningRenderPass'; +import { createSharpeningAndSmoothingRenderPass } from './sharpeningRenderPass'; -export { createSharpeningRenderPass }; +export { createSharpeningAndSmoothingRenderPass }; diff --git a/packages/core/src/RenderingEngine/renderPasses/sharpeningRenderPass.ts b/packages/core/src/RenderingEngine/renderPasses/sharpeningRenderPass.ts index 9ee8e1ca8a..d8ef62d689 100644 --- a/packages/core/src/RenderingEngine/renderPasses/sharpeningRenderPass.ts +++ b/packages/core/src/RenderingEngine/renderPasses/sharpeningRenderPass.ts @@ -11,26 +11,67 @@ import vtkForwardPass from '@kitware/vtk.js/Rendering/OpenGL/ForwardPass'; * @param intensity - Sharpening intensity (0 = no sharpening, higher values = more sharpening) * @returns vtkConvolution2DPass configured for edge enhancement */ -function createSharpeningRenderPass(intensity: number) { +function createSharpeningAndSmoothingRenderPass(intensity: number) { let renderPass = vtkForwardPass.newInstance(); - + const convolutionPass = vtkConvolution2DPass.newInstance(); + convolutionPass.setDelegates([renderPass]); if (intensity > 0) { - const convolutionPass = vtkConvolution2DPass.newInstance(); - convolutionPass.setDelegates([renderPass]); + // Sharpening kernel (Laplacian) const k = Math.max(0, intensity); - - // Edge enhancement kernel type 2 (all 8 neighbors) - // This kernel detects edges in all directions and enhances them - // The center value (1 + 8*k) ensures the image brightness is maintained - // while edges are enhanced proportionally to the intensity parameter - convolutionPass.setKernelDimension(3); convolutionPass.setKernel([-k, -k, -k, -k, 1 + 8 * k, -k, -k, -k, -k]); + } else if (intensity < 0) { + const smoothStrength = Math.min(Math.abs(intensity), 1000); + + // Generate a 15x15 Gaussian blur kernel (σ ≈ 5.0) + const gaussianKernel = createGaussianKernel(15, 5.0); - renderPass = convolutionPass; + // Identity kernel (15x15 → center=1, rest=0) + const identityKernel: number[] = Array(225).fill(0); + identityKernel[112] = 1; // center index + // Blend strength + const alpha = Math.min(smoothStrength / 10, 1.0); + + // Blend between identity and Gaussian + const kernel = gaussianKernel.map( + (g, i) => (1 - alpha) * identityKernel[i] + alpha * g + ); + + convolutionPass.setKernelDimension(15); + convolutionPass.setKernel(kernel); } + renderPass = convolutionPass; return renderPass; } -export { createSharpeningRenderPass }; +/** + * Creates a normalized 2D Gaussian kernel for image smoothing. + * + * The Gaussian kernel is used for blurring images by averaging pixel values + * with their neighbors, weighted by a Gaussian function. The kernel size and + * standard deviation (sigma) control the amount and spread of smoothing. + * + * @param size - The width and height of the square kernel (e.g., 3, 5, 15). + * @param sigma - The standard deviation of the Gaussian distribution (controls blur strength). + * @returns A flattened array of kernel weights, normalized so their sum is 1. + */ +function createGaussianKernel(size: number, sigma: number): number[] { + const kernel = []; + const mean = (size - 1) / 2; + let sum = 0; + + for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + const dx = x - mean; + const dy = y - mean; + const value = Math.exp(-(dx * dx + dy * dy) / (2 * Math.pow(sigma, 2))); + kernel.push(value); + sum += value; + } + } + + return kernel.map((v) => v / sum); +} + +export { createSharpeningAndSmoothingRenderPass }; From 180db0fc49d8bcfb972dca99bbc11cd2daa141cc Mon Sep 17 00:00:00 2001 From: 1532 Date: Thu, 25 Sep 2025 13:56:50 +0530 Subject: [PATCH 2/4] feat(image): image smoothing for stack/volume viewports --- packages/core/examples/smoothing/index.ts | 477 ++++++++++++++++++ .../src/RenderingEngine/BaseVolumeViewport.ts | 42 +- .../core/src/RenderingEngine/StackViewport.ts | 34 +- .../src/RenderingEngine/renderPasses/index.ts | 3 +- .../renderPasses/smoothingRenderPass.ts | 73 +++ packages/core/src/types/ViewportProperties.ts | 1 + utils/ExampleRunner/example-info.json | 4 + 7 files changed, 621 insertions(+), 13 deletions(-) create mode 100644 packages/core/examples/smoothing/index.ts create mode 100644 packages/core/src/RenderingEngine/renderPasses/smoothingRenderPass.ts diff --git a/packages/core/examples/smoothing/index.ts b/packages/core/examples/smoothing/index.ts new file mode 100644 index 0000000000..e8430f73c7 --- /dev/null +++ b/packages/core/examples/smoothing/index.ts @@ -0,0 +1,477 @@ +import type { Types } from '@cornerstonejs/core'; +import { + RenderingEngine, + Enums, + getRenderingEngine, + volumeLoader, +} from '@cornerstonejs/core'; +import { + initDemo, + createImageIdsAndCacheMetaData, + setTitleAndDescription, + addButtonToToolbar, + addSliderToToolbar, + ctVoiRange, + setCtTransferFunctionForVolumeActor, +} from '../../../../utils/demo/helpers'; +import * as cornerstoneTools from '@cornerstonejs/tools'; + +// This is for debugging purposes +console.warn( + 'Click on index.ts to open source code for this example --------->' +); + +const { ViewportType } = Enums; + +// ======== Constants ======= // +const renderingEngineId = 'myRenderingEngine'; +const stackViewportId = 'CT_STACK'; +const stackViewportId2 = 'CT_STACK_2'; +const stackViewportId3 = 'CT_STACK_3'; +const volumeViewportId = 'CT_VOLUME'; +const toolGroupId = 'myToolGroup'; + +// ======== Set up page ======== // +setTitleAndDescription( + 'Image Smoothing', + 'Demonstrates image smoothing using Gaussian blur kernel for Stack & Volume Viewports.' +); + +const content = document.getElementById('content'); +const viewportsContainer = document.createElement('div'); +viewportsContainer.style.display = 'flex'; +viewportsContainer.style.flexDirection = 'row'; +viewportsContainer.style.gap = '10px'; + +content.appendChild(viewportsContainer); + +// Create stack viewport element +const stackElement = document.createElement('div'); +stackElement.id = 'cornerstone-stack-element'; +stackElement.style.width = '400px'; +stackElement.style.height = '400px'; + +viewportsContainer.appendChild(stackElement); + +// Create second stack viewport element +const stackElement2 = document.createElement('div'); +stackElement2.id = 'cornerstone-stack-element-2'; +stackElement2.style.width = '400px'; +stackElement2.style.height = '400px'; + +viewportsContainer.appendChild(stackElement2); + +// Create third stack viewport element +const stackElement3 = document.createElement('div'); +stackElement3.id = 'cornerstone-stack-element-3'; +stackElement3.style.width = '400px'; +stackElement3.style.height = '400px'; + +viewportsContainer.appendChild(stackElement3); + +// Create volume viewport element +const volumeElement = document.createElement('div'); +volumeElement.id = 'cornerstone-volume-element'; +volumeElement.style.width = '400px'; +volumeElement.style.height = '400px'; + +viewportsContainer.appendChild(volumeElement); + +// Disable right-click context menu on all viewport elements +[stackElement, stackElement2, stackElement3, volumeElement].forEach( + (element) => { + element.addEventListener('contextmenu', (e) => { + e.preventDefault(); + return false; + }); + } +); + +// Add labels +const stackLabel = document.createElement('div'); +stackLabel.innerText = 'Stack Viewport 1'; +stackLabel.style.textAlign = 'center'; +stackLabel.style.marginTop = '10px'; +stackElement.appendChild(stackLabel); + +const stackLabel2 = document.createElement('div'); +stackLabel2.innerText = 'Stack Viewport 2'; +stackLabel2.style.textAlign = 'center'; +stackLabel2.style.marginTop = '10px'; +stackElement2.appendChild(stackLabel2); + +const stackLabel3 = document.createElement('div'); +stackLabel3.innerText = 'Stack Viewport 3'; +stackLabel3.style.textAlign = 'center'; +stackLabel3.style.marginTop = '10px'; +stackElement3.appendChild(stackLabel3); + +const volumeLabel = document.createElement('div'); +volumeLabel.innerText = 'Volume Viewport'; +volumeLabel.style.textAlign = 'center'; +volumeLabel.style.marginTop = '10px'; +volumeElement.appendChild(volumeLabel); + +const info = document.createElement('div'); +content.appendChild(info); + +const smoothingInfo = document.createElement('div'); +info.appendChild(smoothingInfo); +smoothingInfo.innerText = 'Smoothing: 0%'; + +// Add interaction instructions +const instructionsContainer = document.createElement('div'); +instructionsContainer.style.marginTop = '20px'; +instructionsContainer.style.padding = '10px'; +instructionsContainer.style.backgroundColor = '#f0f0f0'; +instructionsContainer.style.borderRadius = '5px'; +content.appendChild(instructionsContainer); + +const instructionsTitle = document.createElement('h3'); +instructionsTitle.innerText = 'Interaction Instructions:'; +instructionsTitle.style.marginTop = '0'; +instructionsContainer.appendChild(instructionsTitle); + +const instructionsList = document.createElement('ul'); +instructionsList.style.marginTop = '10px'; +instructionsList.innerHTML = ` +
  • Left Click + Drag: Stack Scroll (navigate through slices)
  • +
  • Right Click + Drag: Zoom In/Out
  • +
  • Middle Click + Drag: Pan (move the image)
  • +
  • Mouse Wheel: Stack Scroll (navigate through slices)
  • +
  • Smoothing Slider: Adjust image Smoothing (0 to -1000%)
  • +`; +instructionsContainer.appendChild(instructionsList); + +// Add Smoothing slider with a unique ID so we can reference it later +addSliderToToolbar({ + id: 'smoothing-slider', + title: 'Smoothing', + range: [-1000, 0], + defaultValue: 0, + onSelectedValueChange: (value: number) => { + // Get the rendering engine + const renderingEngine = getRenderingEngine(renderingEngineId); + + // Update stack viewport 1 + const stackViewport = renderingEngine.getViewport( + stackViewportId + ) as Types.IStackViewport; + + if (stackViewport) { + stackViewport.setProperties({ + smoothing: value / 100, // Convert percentage to decimal + }); + stackViewport.render(); + } + + // Update stack viewport 2 + const stackViewport2 = renderingEngine.getViewport( + stackViewportId2 + ) as Types.IStackViewport; + + if (stackViewport2) { + stackViewport2.setProperties({ + smoothing: value / 100, // Convert percentage to decimal + }); + stackViewport2.render(); + } + + // Update stack viewport 3 + const stackViewport3 = renderingEngine.getViewport( + stackViewportId3 + ) as Types.IStackViewport; + + if (stackViewport3) { + stackViewport3.setProperties({ + smoothing: value / 100, // Convert percentage to decimal + }); + stackViewport3.render(); + } + + // Update volume viewport + const volumeViewport = renderingEngine.getViewport( + volumeViewportId + ) as Types.IVolumeViewport; + + if (volumeViewport) { + volumeViewport.setProperties({ + smoothing: value / 100, // Convert percentage to decimal + }); + volumeViewport.render(); + } + + smoothingInfo.innerText = `Smoothing: ${value}%`; + }, +}); + +addButtonToToolbar({ + title: 'Reset', + onClick: () => { + // Reset the slider value + const slider = document.getElementById( + 'smoothing-slider' + ) as HTMLInputElement; + if (slider) { + slider.value = '0'; + } + + // Get the rendering engine + const renderingEngine = getRenderingEngine(renderingEngineId); + + // Reset stack viewport 1 + const stackViewport = renderingEngine.getViewport( + stackViewportId + ) as Types.IStackViewport; + + if (stackViewport) { + stackViewport.setProperties({ + smoothing: 0, + }); + stackViewport.resetProperties(); + stackViewport.render(); + } + + // Reset stack viewport 2 + const stackViewport2 = renderingEngine.getViewport( + stackViewportId2 + ) as Types.IStackViewport; + + if (stackViewport2) { + stackViewport2.setProperties({ + smoothing: 0, + }); + stackViewport2.resetProperties(); + stackViewport2.render(); + } + + // Reset stack viewport 3 + const stackViewport3 = renderingEngine.getViewport( + stackViewportId3 + ) as Types.IStackViewport; + + if (stackViewport3) { + stackViewport3.setProperties({ + smoothing: 0, + }); + stackViewport3.resetProperties(); + stackViewport3.render(); + } + + // Reset volume viewport + const volumeViewport = renderingEngine.getViewport( + volumeViewportId + ) as Types.IVolumeViewport; + + if (volumeViewport) { + volumeViewport.setProperties({ + smoothing: 0, + }); + volumeViewport.resetProperties(); + volumeViewport.render(); + } + + smoothingInfo.innerText = 'Smoothing: 0%'; + }, +}); + +/** + * Runs the demo + */ +async function run() { + // Init Cornerstone and related libraries + await initDemo(); + + // Initialize cornerstone tools + const { ToolGroupManager, StackScrollTool, ZoomTool, PanTool } = + cornerstoneTools; + + // Add tools to Cornerstone3D + cornerstoneTools.addTool(StackScrollTool); + cornerstoneTools.addTool(ZoomTool); + cornerstoneTools.addTool(PanTool); + + // Get Cornerstone imageIds and fetch metadata into RAM for first series + const imageIds = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463', + SeriesInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561', + wadoRsRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + }); + + // Get imageIds for second series + const imageIds2 = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.99.1071.55651399101931177647030363790032', + SeriesInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.99.1071.87075509829481869121008947712950', + wadoRsRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + }); + + // Get imageIds for third series + const imageIds3 = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.1.84416332615988066829602832830236187384', + SeriesInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.1.73259459389408720224591489579010582581', + wadoRsRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + }); + + // Instantiate a rendering engine + const renderingEngine = new RenderingEngine(renderingEngineId); + + // Create the viewports + const viewportInputArray = [ + { + viewportId: stackViewportId, + type: ViewportType.STACK, + element: stackElement, + defaultOptions: { + background: [0.2, 0, 0.2] as Types.Point3, + }, + }, + { + viewportId: stackViewportId2, + type: ViewportType.STACK, + element: stackElement2, + defaultOptions: { + background: [0.2, 0, 0.2] as Types.Point3, + }, + }, + { + viewportId: stackViewportId3, + type: ViewportType.STACK, + element: stackElement3, + defaultOptions: { + background: [0.2, 0, 0.2] as Types.Point3, + }, + }, + { + viewportId: volumeViewportId, + type: ViewportType.ORTHOGRAPHIC, + element: volumeElement, + defaultOptions: { + orientation: Enums.OrientationAxis.SAGITTAL, + background: [0.2, 0, 0.2] as Types.Point3, + }, + }, + ]; + + renderingEngine.setViewports(viewportInputArray); + + // Get the stack viewport + const stackViewport = renderingEngine.getViewport( + stackViewportId + ) as Types.IStackViewport; + + // Set the stack on the viewport + await stackViewport.setStack(imageIds); + + // Set the VOI range + stackViewport.setProperties({ voiRange: ctVoiRange }); + + // Render the stack viewport + stackViewport.render(); + + // Get the second stack viewport + const stackViewport2 = renderingEngine.getViewport( + stackViewportId2 + ) as Types.IStackViewport; + + // Set the stack on the second viewport + await stackViewport2.setStack(imageIds2); + + // Set the VOI range with custom window/level for this viewport + stackViewport2.setProperties({ + voiRange: { + lower: 2000 - 4100 / 2, // Level - Window/2 + upper: 2000 + 4100 / 2, // Level + Window/2 + }, + }); + + // Render the second stack viewport + stackViewport2.render(); + + // Get the third stack viewport + const stackViewport3 = renderingEngine.getViewport( + stackViewportId3 + ) as Types.IStackViewport; + + // Set the stack on the third viewport + await stackViewport3.setStack(imageIds3); + + // Set the VOI range with custom window/level for this viewport + stackViewport3.setProperties({ + voiRange: { + lower: 5393 - 1751 / 2, // Level - Window/2 + upper: 5393 + 1751 / 2, // Level + Window/2 + }, + }); + + // Render the third stack viewport + stackViewport3.render(); + + // Get the volume viewport + const volumeViewport = renderingEngine.getViewport( + volumeViewportId + ) as Types.IVolumeViewport; + + // Define a unique id for the volume + const volumeName = 'CT_VOLUME_ID'; + const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; + const volumeId = `${volumeLoaderScheme}:${volumeName}`; + + // Define a volume in memory + const volume = await volumeLoader.createAndCacheVolume(volumeId, { + imageIds, + }); + + // Set the volume to load + volume.load(); + + // Set the volume on the viewport + volumeViewport.setVolumes([ + { volumeId, callback: setCtTransferFunctionForVolumeActor }, + ]); + + // Render the volume viewport + volumeViewport.render(); + + // Create a tool group + const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); + + // Add all viewports to the tool group + toolGroup.addViewport(stackViewportId, renderingEngineId); + toolGroup.addViewport(stackViewportId2, renderingEngineId); + toolGroup.addViewport(stackViewportId3, renderingEngineId); + toolGroup.addViewport(volumeViewportId, renderingEngineId); + + // Add all tools to the tool group + toolGroup.addTool(StackScrollTool.toolName); + toolGroup.addTool(ZoomTool.toolName); + toolGroup.addTool(PanTool.toolName); + + // Set up tool bindings + // Left click (Primary) - Stack Scroll + toolGroup.setToolActive(StackScrollTool.toolName, { + bindings: [{ mouseButton: cornerstoneTools.Enums.MouseBindings.Primary }], + }); + + // Right click (Secondary) - Zoom + toolGroup.setToolActive(ZoomTool.toolName, { + bindings: [{ mouseButton: cornerstoneTools.Enums.MouseBindings.Secondary }], + }); + + // Middle click (Auxiliary/Middle) - Pan + toolGroup.setToolActive(PanTool.toolName, { + bindings: [{ mouseButton: cornerstoneTools.Enums.MouseBindings.Auxiliary }], + }); + + // Mouse wheel - Stack Scroll + toolGroup.setToolActive(StackScrollTool.toolName, { + bindings: [{ mouseButton: cornerstoneTools.Enums.MouseBindings.Wheel }], + }); +} + +run(); diff --git a/packages/core/src/RenderingEngine/BaseVolumeViewport.ts b/packages/core/src/RenderingEngine/BaseVolumeViewport.ts index fb28a4f6ae..5025f73bea 100644 --- a/packages/core/src/RenderingEngine/BaseVolumeViewport.ts +++ b/packages/core/src/RenderingEngine/BaseVolumeViewport.ts @@ -77,7 +77,10 @@ import { isContextPoolRenderingEngine } from './helpers/isContextPoolRenderingEn import type vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer'; import mprCameraValues from '../constants/mprCameraValues'; import { isInvalidNumber } from './helpers/isInvalidNumber'; -import { createSharpeningRenderPass } from './renderPasses'; +import { + createSharpeningRenderPass, + createSmoothingRenderPass, +} from './renderPasses'; /** * Abstract base class for volume viewports. VolumeViewports are used to render * 3D volumes from which various orientations can be viewed. Since VolumeViewports @@ -91,6 +94,7 @@ abstract class BaseVolumeViewport extends Viewport { useCPURendering = false; private _FrameOfReferenceUID: string; private sharpening: number = 0; + private smoothing: number = 0; protected initialTransferFunctionNodes: TransferFunctionNodes; // Viewport Properties @@ -897,7 +901,11 @@ abstract class BaseVolumeViewport extends Viewport { [-viewPlaneNormal[0], -viewPlaneNormal[1], -viewPlaneNormal[2]], projectedDistance ); - const focalShift = vec3.subtract(vec3.create(), newImagePositionPatient, focalPoint); + const focalShift = vec3.subtract( + vec3.create(), + newImagePositionPatient, + focalPoint + ); const newPosition = vec3.add(vec3.create(), position, focalShift); // this.setViewReference({ // ...viewRef, @@ -905,7 +913,7 @@ abstract class BaseVolumeViewport extends Viewport { // }); this.setCamera({ focalPoint: newImagePositionPatient as Point3, - position: newPosition as Point3 + position: newPosition as Point3, }); this.render(); return; @@ -993,6 +1001,7 @@ abstract class BaseVolumeViewport extends Viewport { slabThickness, sampleDistanceMultiplier, sharpening, + smoothing, }: VolumeViewportProperties = {}, volumeId?: string, suppressEvents = false @@ -1053,6 +1062,9 @@ abstract class BaseVolumeViewport extends Viewport { if (typeof sharpening !== 'undefined') { this.setSharpening(sharpening); } + if (typeof smoothing !== 'undefined') { + this.setSmoothing(smoothing); + } } /** @@ -1064,18 +1076,27 @@ abstract class BaseVolumeViewport extends Viewport { this.sharpening = sharpening; this.render(); }; + /** + * Sets the smoothing for the current viewport. + * @param smoothing - The smoothing configuration to use. + */ + private setSmoothing = (smoothing: number): void => { + // Store smoothing settings directly on the class + this.smoothing = smoothing; + this.render(); + }; /** * Check if custom render passes should be used for this viewport. * @returns True if custom render passes should be used, false otherwise */ protected shouldUseCustomRenderPass(): boolean { - return this.sharpening > 0 && !this.useCPURendering; + return (this.sharpening > 0 || this.smoothing < 0) && !this.useCPURendering; } /** * Get render passes for this viewport. - * If sharpening is enabled, returns appropriate render passes. + * If sharpening or smoothing is enabled, returns appropriate render passes. * @returns Array of VTK render passes or null if no custom passes are needed */ public getRenderPasses = () => { @@ -1084,9 +1105,15 @@ abstract class BaseVolumeViewport extends Viewport { } try { - return [createSharpeningRenderPass(this.sharpening)]; + if (this.sharpening > 0) { + return [createSharpeningRenderPass(this.sharpening)]; + } else if (this.smoothing < 0) { + return [createSmoothingRenderPass(this.smoothing)]; + } else { + return null; + } } catch (e) { - console.warn('Failed to create sharpening render passes:', e); + console.warn('Failed to create custom render passes:', e); return null; } }; @@ -1260,6 +1287,7 @@ abstract class BaseVolumeViewport extends Viewport { slabThickness: slabThickness, preset, sharpening: this.sharpening, + smoothing: this.smoothing, }; }; diff --git a/packages/core/src/RenderingEngine/StackViewport.ts b/packages/core/src/RenderingEngine/StackViewport.ts index 6392f56906..3bc9c1c928 100644 --- a/packages/core/src/RenderingEngine/StackViewport.ts +++ b/packages/core/src/RenderingEngine/StackViewport.ts @@ -98,7 +98,10 @@ import getSpacingInNormalDirection from '../utilities/getSpacingInNormalDirectio import getClosestImageId from '../utilities/getClosestImageId'; import { adjustInitialViewUp } from '../utilities/adjustInitialViewUp'; import { isContextPoolRenderingEngine } from './helpers/isContextPoolRenderingEngine'; -import { createSharpeningRenderPass } from './renderPasses'; +import { + createSharpeningRenderPass, + createSmoothingRenderPass, +} from './renderPasses'; export interface ImageDataMetaData { bitsAllocated: number; @@ -167,6 +170,7 @@ class StackViewport extends Viewport { private voiRange: VOIRange; private voiUpdatedWithSetProperties = false; private sharpening: number = 0; + private smoothing: number = 0; private VOILUTFunction: VOILUTFunctionType; // private invert = false; @@ -439,13 +443,21 @@ class StackViewport extends Viewport { this.render(); }; - + /** + * Sets the smoothing for the current viewport. + * @param smoothing - The smoothing configuration to use. + */ + private setSmoothing = (smoothing: number): void => { + // Store smoothing settings directly on the class + this.smoothing = smoothing; + this.render(); + }; /** * Check if custom render passes should be used for this viewport. * @returns True if custom render passes should be used, false otherwise */ protected shouldUseCustomRenderPass(): boolean { - return this.sharpening > 0 && !this.useCPURendering; + return (this.sharpening > 0 || this.smoothing < 0) && !this.useCPURendering; } /** @@ -459,9 +471,15 @@ class StackViewport extends Viewport { } try { - return [createSharpeningRenderPass(this.sharpening)]; + if (this.sharpening > 0) { + return [createSharpeningRenderPass(this.sharpening)]; + } else if (this.smoothing < 0) { + return [createSmoothingRenderPass(this.smoothing)]; + } else { + return null; + } } catch (e) { - console.warn('Failed to create sharpening render passes:', e); + console.warn('Failed to create custom render passes:', e); return null; } }; @@ -723,6 +741,7 @@ class StackViewport extends Viewport { invert, interpolationType, sharpening, + smoothing, }: StackViewportProperties = {}, suppressEvents = false ): void { @@ -741,6 +760,7 @@ class StackViewport extends Viewport { interpolationType: this.globalDefaultProperties.interpolationType ?? interpolationType, sharpening: this.globalDefaultProperties.sharpening ?? sharpening, + smoothing: this.globalDefaultProperties.smoothing ?? smoothing, }; if (typeof colormap !== 'undefined') { @@ -768,6 +788,9 @@ class StackViewport extends Viewport { if (typeof sharpening !== 'undefined') { this.setSharpening(sharpening); } + if (typeof smoothing !== 'undefined') { + this.setSmoothing(smoothing); + } } /** @@ -813,6 +836,7 @@ class StackViewport extends Viewport { invert, isComputedVOI: !voiUpdatedWithSetProperties, sharpening: this.sharpening, + smoothing: this.smoothing, }; }; diff --git a/packages/core/src/RenderingEngine/renderPasses/index.ts b/packages/core/src/RenderingEngine/renderPasses/index.ts index 2bc95c12a0..794d0b89ff 100644 --- a/packages/core/src/RenderingEngine/renderPasses/index.ts +++ b/packages/core/src/RenderingEngine/renderPasses/index.ts @@ -1,3 +1,4 @@ import { createSharpeningRenderPass } from './sharpeningRenderPass'; +import { createSmoothingRenderPass } from './smoothingRenderPass'; -export { createSharpeningRenderPass }; +export { createSharpeningRenderPass, createSmoothingRenderPass }; diff --git a/packages/core/src/RenderingEngine/renderPasses/smoothingRenderPass.ts b/packages/core/src/RenderingEngine/renderPasses/smoothingRenderPass.ts new file mode 100644 index 0000000000..58ab26e176 --- /dev/null +++ b/packages/core/src/RenderingEngine/renderPasses/smoothingRenderPass.ts @@ -0,0 +1,73 @@ +import vtkConvolution2DPass from '@kitware/vtk.js/Rendering/OpenGL/Convolution2DPass'; +import vtkForwardPass from '@kitware/vtk.js/Rendering/OpenGL/ForwardPass'; + +/** + * Creates a GPU-based convolution pass for image smoothing. + * + * The smoothing works by applying a Gaussian blur kernel to the image, + * which averages pixel values with their neighbors, effectively reducing noise + * and softening edges. The intensity parameter controls the strength of the smoothing effect. + * + * @param intensity - Smoothing intensity (0 = no smoothing, negative values = more smoothing) + * @returns vtkConvolution2DPass configured for edge enhancement + */ +function createSmoothingRenderPass(intensity: number) { + let renderPass = vtkForwardPass.newInstance(); + + if (intensity < 0) { + const convolutionPass = vtkConvolution2DPass.newInstance(); + convolutionPass.setDelegates([renderPass]); + const smoothStrength = Math.min(Math.abs(intensity), 1000); + + // Generate a 15x15 Gaussian blur kernel (σ ≈ 5.0) + const gaussianKernel = createGaussianKernel(15, 5.0); + + // Identity kernel (15x15 → center=1, rest=0) + const identityKernel: number[] = Array(225).fill(0); + identityKernel[112] = 1; // center index + // Blend strength + const alpha = Math.min(smoothStrength / 10, 1.0); + + // Blend between identity and Gaussian + const kernel = gaussianKernel.map( + (g, i) => (1 - alpha) * identityKernel[i] + alpha * g + ); + + convolutionPass.setKernelDimension(15); + convolutionPass.setKernel(kernel); + renderPass = convolutionPass; + } + + return renderPass; +} + +/** + * Creates a normalized 2D Gaussian kernel for image smoothing. + * + * The Gaussian kernel is used for blurring images by averaging pixel values + * with their neighbors, weighted by a Gaussian function. The kernel size and + * standard deviation (sigma) control the amount and spread of smoothing. + * + * @param size - The width and height of the square kernel (e.g., 3, 5, 15). + * @param sigma - The standard deviation of the Gaussian distribution (controls blur strength). + * @returns A flattened array of kernel weights, normalized so their sum is 1. + */ +function createGaussianKernel(size: number, sigma: number): number[] { + const kernel = []; + const mean = (size - 1) / 2; + let sum = 0; + + for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + const dx = x - mean; + const dy = y - mean; + const value = Math.exp(-(dx * dx + dy * dy) / (2 * Math.pow(sigma, 2))); + kernel.push(value); + sum += value; + } + } + + return kernel.map((v) => v / sum); +} + +export { createSmoothingRenderPass }; diff --git a/packages/core/src/types/ViewportProperties.ts b/packages/core/src/types/ViewportProperties.ts index d5e166b191..43f4786e29 100644 --- a/packages/core/src/types/ViewportProperties.ts +++ b/packages/core/src/types/ViewportProperties.ts @@ -23,4 +23,5 @@ export interface ViewportProperties { /** Image sharpening settings */ sharpening?: number; + smoothing?: number; } diff --git a/utils/ExampleRunner/example-info.json b/utils/ExampleRunner/example-info.json index a34b3bf02b..2d77fdfa7f 100644 --- a/utils/ExampleRunner/example-info.json +++ b/utils/ExampleRunner/example-info.json @@ -172,6 +172,10 @@ "sharpening": { "name": "Image Sharpening For Stack & Volume Viewports", "description": "Demonstrates how to apply image sharpening to stack and volume viewports" + }, + "smoothing": { + "name": "Image Smoothing For Stack & Volume Viewports", + "description": "Demonstrates how to apply image smoothing to stack and volume viewports" } }, "tools-basic": { From 790e5f3c375f52ba7996f8457de332c81b6f67c1 Mon Sep 17 00:00:00 2001 From: 1532 Date: Thu, 25 Sep 2025 14:15:19 +0530 Subject: [PATCH 3/4] Changed range of sharpening --- packages/core/examples/sharpening/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/examples/sharpening/index.ts b/packages/core/examples/sharpening/index.ts index 8c0adc4bfe..e2424ce2e5 100644 --- a/packages/core/examples/sharpening/index.ts +++ b/packages/core/examples/sharpening/index.ts @@ -147,7 +147,7 @@ instructionsContainer.appendChild(instructionsList); addSliderToToolbar({ id: 'sharpening-slider', title: 'Sharpening', - range: [-1000, 1000], + range: [0, 300], defaultValue: 0, onSelectedValueChange: (value: number) => { // Get the rendering engine From 363b645ec431556605794955e589aba027bd5758 Mon Sep 17 00:00:00 2001 From: suryadas-trenser Date: Wed, 8 Oct 2025 06:49:47 +0000 Subject: [PATCH 4/4] ensure smoothing is active only when smoothing property of viewport greater than 0 --- packages/core/examples/smoothing/index.ts | 2 +- .../src/RenderingEngine/BaseVolumeViewport.ts | 15 +++++++++------ .../core/src/RenderingEngine/StackViewport.ts | 17 ++++++++++------- .../renderPasses/smoothingRenderPass.ts | 19 +++++++++++-------- packages/core/src/types/ViewportProperties.ts | 1 + 5 files changed, 32 insertions(+), 22 deletions(-) diff --git a/packages/core/examples/smoothing/index.ts b/packages/core/examples/smoothing/index.ts index e8430f73c7..d79d496ae1 100644 --- a/packages/core/examples/smoothing/index.ts +++ b/packages/core/examples/smoothing/index.ts @@ -147,7 +147,7 @@ instructionsContainer.appendChild(instructionsList); addSliderToToolbar({ id: 'smoothing-slider', title: 'Smoothing', - range: [-1000, 0], + range: [0, 1000], defaultValue: 0, onSelectedValueChange: (value: number) => { // Get the rendering engine diff --git a/packages/core/src/RenderingEngine/BaseVolumeViewport.ts b/packages/core/src/RenderingEngine/BaseVolumeViewport.ts index 5025f73bea..9c2732d241 100644 --- a/packages/core/src/RenderingEngine/BaseVolumeViewport.ts +++ b/packages/core/src/RenderingEngine/BaseVolumeViewport.ts @@ -1091,7 +1091,7 @@ abstract class BaseVolumeViewport extends Viewport { * @returns True if custom render passes should be used, false otherwise */ protected shouldUseCustomRenderPass(): boolean { - return (this.sharpening > 0 || this.smoothing < 0) && !this.useCPURendering; + return !this.useCPURendering; } /** @@ -1104,14 +1104,17 @@ abstract class BaseVolumeViewport extends Viewport { return null; } + const renderPasses = []; + try { + if (this.smoothing > 0) { + renderPasses.push(createSmoothingRenderPass(this.smoothing)); + } if (this.sharpening > 0) { - return [createSharpeningRenderPass(this.sharpening)]; - } else if (this.smoothing < 0) { - return [createSmoothingRenderPass(this.smoothing)]; - } else { - return null; + renderPasses.push(createSharpeningRenderPass(this.sharpening)); } + + return renderPasses.length ? renderPasses : null; } catch (e) { console.warn('Failed to create custom render passes:', e); return null; diff --git a/packages/core/src/RenderingEngine/StackViewport.ts b/packages/core/src/RenderingEngine/StackViewport.ts index 3bc9c1c928..178ddae45b 100644 --- a/packages/core/src/RenderingEngine/StackViewport.ts +++ b/packages/core/src/RenderingEngine/StackViewport.ts @@ -457,12 +457,12 @@ class StackViewport extends Viewport { * @returns True if custom render passes should be used, false otherwise */ protected shouldUseCustomRenderPass(): boolean { - return (this.sharpening > 0 || this.smoothing < 0) && !this.useCPURendering; + return !this.useCPURendering; } /** * Get render passes for this viewport. - * If sharpening is enabled, returns appropriate render passes. + * If sharpening or smoothing is enabled, returns appropriate render passes. * @returns Array of VTK render passes or null if no custom passes are needed */ public getRenderPasses = () => { @@ -470,14 +470,17 @@ class StackViewport extends Viewport { return null; } + const renderPasses = []; + try { + if (this.smoothing > 0) { + renderPasses.push(createSmoothingRenderPass(this.smoothing)); + } if (this.sharpening > 0) { - return [createSharpeningRenderPass(this.sharpening)]; - } else if (this.smoothing < 0) { - return [createSmoothingRenderPass(this.smoothing)]; - } else { - return null; + renderPasses.push(createSharpeningRenderPass(this.sharpening)); } + + return renderPasses.length ? renderPasses : null; } catch (e) { console.warn('Failed to create custom render passes:', e); return null; diff --git a/packages/core/src/RenderingEngine/renderPasses/smoothingRenderPass.ts b/packages/core/src/RenderingEngine/renderPasses/smoothingRenderPass.ts index 58ab26e176..7bba5b16f8 100644 --- a/packages/core/src/RenderingEngine/renderPasses/smoothingRenderPass.ts +++ b/packages/core/src/RenderingEngine/renderPasses/smoothingRenderPass.ts @@ -8,23 +8,26 @@ import vtkForwardPass from '@kitware/vtk.js/Rendering/OpenGL/ForwardPass'; * which averages pixel values with their neighbors, effectively reducing noise * and softening edges. The intensity parameter controls the strength of the smoothing effect. * - * @param intensity - Smoothing intensity (0 = no smoothing, negative values = more smoothing) - * @returns vtkConvolution2DPass configured for edge enhancement + * @param intensity - Smoothing intensity (0 = no smoothing, positive values = more smoothing) + * @returns vtkConvolution2DPass configured for image smoothing (Gaussian blur) */ function createSmoothingRenderPass(intensity: number) { let renderPass = vtkForwardPass.newInstance(); - if (intensity < 0) { + if (intensity > 0) { const convolutionPass = vtkConvolution2DPass.newInstance(); convolutionPass.setDelegates([renderPass]); - const smoothStrength = Math.min(Math.abs(intensity), 1000); + const smoothStrength = Math.min(intensity, 1000); // Generate a 15x15 Gaussian blur kernel (σ ≈ 5.0) - const gaussianKernel = createGaussianKernel(15, 5.0); - + const kernelSize = 15; + const sigma = 5.0; + const gaussianKernel = createGaussianKernel(kernelSize, sigma); + const totalElements = kernelSize * kernelSize; + const centerIndex = Math.floor(totalElements / 2); // Identity kernel (15x15 → center=1, rest=0) - const identityKernel: number[] = Array(225).fill(0); - identityKernel[112] = 1; // center index + const identityKernel: number[] = Array(totalElements).fill(0); + identityKernel[centerIndex] = 1; // Blend strength const alpha = Math.min(smoothStrength / 10, 1.0); diff --git a/packages/core/src/types/ViewportProperties.ts b/packages/core/src/types/ViewportProperties.ts index 43f4786e29..87c9626eed 100644 --- a/packages/core/src/types/ViewportProperties.ts +++ b/packages/core/src/types/ViewportProperties.ts @@ -23,5 +23,6 @@ export interface ViewportProperties { /** Image sharpening settings */ sharpening?: number; + /** Image smoothing settings */ smoothing?: number; }