packages = ["pillow", "numpy", "pydicom", "pynrrd", "scipy"] Download DICOM Convert Software

Volume Slicer

Loading file(s)...
Ultrasound slice
Click on the ultrasound to place up to two lines.
0.20x
0
0
0
from js import document from pyodide.ffi.wrappers import add_event_listener import numpy as np from PIL import Image from io import BytesIO import base64 import math from js import window import pydicom import nrrd from scipy.ndimage import map_coordinates import tempfile import os volume = None volume_dims = None file_type = None voxel_spacing = np.array([1.0, 1.0, 1.0], dtype=float) slice_viewbox_dims = None slice_viewbox_half_world = None slice_axis_basis = np.eye(3, dtype=float) slice_position_origin_world = None auto_contrast_enabled = False def get_nrrd_spacing(header): space_directions = header.get("space directions") if space_directions is None: return np.array([1.0, 1.0, 1.0], dtype=float) spacings = [] for direction in space_directions: if direction is None: spacings.append(1.0) continue vec = np.array(direction, dtype=float) spacings.append(float(np.linalg.norm(vec))) if len(spacings) < 3: spacings.extend([1.0] * (3 - len(spacings))) return np.array(spacings[:3], dtype=float) def normalize_to_uint8(arr): if arr.dtype == np.uint8: return arr arr = arr.astype(np.float32) m = arr.min() M = arr.max() if M > m: arr = (255.0 * (arr - m) / (M - m)).astype(np.uint8) else: arr = np.zeros_like(arr, dtype=np.uint8) return arr def maximize_contrast(arr): arr = arr.astype(np.float32) finite = arr[np.isfinite(arr)] if finite.size == 0: return np.zeros_like(arr, dtype=np.uint8) visible = finite[finite > 0] if visible.size >= 16: finite = visible lo, hi = np.percentile(finite, [1.0, 99.7]) if hi <= lo: lo = finite.min() hi = finite.max() if hi <= lo: return np.zeros_like(arr, dtype=np.uint8) arr = np.clip((arr - lo) * 255.0 / (hi - lo), 0, 255) return arr.astype(np.uint8) def normalize_vector(vec, fallback): length = float(np.linalg.norm(vec)) if length < 1e-8: return np.array(fallback, dtype=float) return vec / length def get_slider_rotation_matrix(): angle_x = float(document.getElementById("angle-x-slider").value) angle_y = float(document.getElementById("angle-y-slider").value) angle_z = float(document.getElementById("angle-z-slider").value) ax = math.radians(angle_x) ay = math.radians(angle_y) az = math.radians(angle_z) Rx = np.array( [ [1, 0, 0], [0, math.cos(ax), -math.sin(ax)], [0, math.sin(ax), math.cos(ax)], ], dtype=float, ) Ry = np.array( [ [math.cos(ay), 0, math.sin(ay)], [0, 1, 0], [-math.sin(ay), 0, math.cos(ay)], ], dtype=float, ) Rz = np.array( [ [math.cos(az), -math.sin(az), 0], [math.sin(az), math.cos(az), 0], [0, 0, 1], ], dtype=float, ) return Rz.dot(Ry).dot(Rx) def get_current_origin_world(nX, nY, nZ, spacing_x, spacing_y, spacing_z): z_pos = int(document.getElementById("z-slider").value) x_pos = int(document.getElementById("x-slider").value) y_pos = int(document.getElementById("y-slider").value) if slice_position_origin_world is None: center_index = np.array( [(nX - 1) / 2.0, (nY - 1) / 2.0, (nZ - 1) / 2.0], dtype=float, ) offset_index = np.array([x_pos, y_pos, z_pos], dtype=float) - center_index base_world = np.array( [ center_index[0] * spacing_x, center_index[1] * spacing_y, center_index[2] * spacing_z, ], dtype=float, ) else: offset_index = np.array([x_pos, y_pos, z_pos], dtype=float) base_world = slice_position_origin_world offset_world = np.array( [ offset_index[0] * spacing_x, offset_index[1] * spacing_y, offset_index[2] * spacing_z, ], dtype=float, ) return base_world + slice_axis_basis.dot(offset_world) def get_volume_shape(): vol = volume if vol.ndim == 4: nZ, nY, nX = vol.shape[:3] channels = vol.shape[3] elif vol.ndim == 3: nZ, nY, nX = vol.shape channels = 1 else: nY, nX = vol.shape nZ = 1 channels = 1 vol = vol.reshape(1, nY, nX) return vol, nZ, nY, nX, channels def set_volume_axis_basis(axes=None): global slice_axis_basis, slice_position_origin_world if volume is None: return vol, nZ, nY, nX, channels = get_volume_shape() spacing_x = float(voxel_spacing[0]) spacing_y = float(voxel_spacing[1]) spacing_z = float(voxel_spacing[2]) slice_position_origin_world = get_current_origin_world( nX, nY, nZ, spacing_x, spacing_y, spacing_z ) slice_axis_basis = slice_axis_basis.dot(get_slider_rotation_matrix()) def reset_volume_axis_basis(): global slice_axis_basis, slice_position_origin_world slice_axis_basis = np.eye(3, dtype=float) slice_position_origin_world = None def extract_slice(): global volume, volume_dims, voxel_spacing global slice_viewbox_dims, slice_viewbox_half_world global slice_axis_basis, slice_position_origin_world if volume is None: return z_pos = int(document.getElementById("z-slider").value) x_pos = int(document.getElementById("x-slider").value) y_pos = int(document.getElementById("y-slider").value) angle_x = float(document.getElementById("angle-x-slider").value) angle_y = float(document.getElementById("angle-y-slider").value) angle_z = float(document.getElementById("angle-z-slider").value) R = slice_axis_basis.dot(get_slider_rotation_matrix()) vol, nZ, nY, nX, channels = get_volume_shape() spacing_x = float(voxel_spacing[0]) spacing_y = float(voxel_spacing[1]) spacing_z = float(voxel_spacing[2]) u_axis = R.dot(np.array([1.0, 0.0, 0.0], dtype=float)) v_axis = R.dot(np.array([0.0, 1.0, 0.0], dtype=float)) if slice_viewbox_dims is None: slice_viewbox_dims = (nX, nY) if slice_viewbox_half_world is None: slice_viewbox_half_world = ( max(((nX - 1) / 2.0) * spacing_x, spacing_x / 2.0, 1.0), max(((nY - 1) / 2.0) * spacing_y, spacing_y / 2.0, 1.0), ) out_w, out_h = slice_viewbox_dims u_extent, v_extent = slice_viewbox_half_world u = np.linspace(-u_extent, u_extent, out_w) v = np.linspace(-v_extent, v_extent, out_h) U, V = np.meshgrid(u, v, indexing="xy") origin_world = get_current_origin_world( nX, nY, nZ, spacing_x, spacing_y, spacing_z ) P_world = ( origin_world.reshape(1, 1, 3) + U[..., np.newaxis] * u_axis.reshape(1, 1, 3) + V[..., np.newaxis] * v_axis.reshape(1, 1, 3) ) X_coords = (P_world[..., 0] / spacing_x).ravel() Y_coords = (P_world[..., 1] / spacing_y).ravel() Z_coords = (P_world[..., 2] / spacing_z).ravel() coords = np.array([Z_coords, Y_coords, X_coords]) if channels == 1: slice_img = map_coordinates( vol, coords, order=1, mode="constant", cval=0, ).reshape(out_h, out_w) else: slices = [] for c in range(channels): ch = map_coordinates( vol[..., c], coords, order=1, mode="constant", cval=0, ).reshape(out_h, out_w) slices.append(ch) slice_img = np.stack(slices, axis=-1) slice_img = normalize_to_uint8(slice_img) if auto_contrast_enabled: slice_img = maximize_contrast(slice_img) pil_im = Image.fromarray(slice_img) buffer = BytesIO() pil_im.save(buffer, format="PNG") encoded = base64.b64encode(buffer.getvalue()).decode("ascii") data_url = f"data:image/png;base64,{encoded}" img_elem = document.getElementById("slice-image") img_elem.src = data_url on_slice_rendered = getattr(window, "onSliceRendered", None) if on_slice_rendered is not None: on_slice_rendered() document.getElementById("z-value").innerText = f"{z_pos}" document.getElementById("x-value").innerText = f"{x_pos}" document.getElementById("y-value").innerText = f"{y_pos}" document.getElementById("angle-x-value").innerText = f"{angle_x:.1f}°" document.getElementById("angle-y-value").innerText = f"{angle_y:.1f}°" document.getElementById("angle-z-value").innerText = f"{angle_z:.1f}°" def toggle_auto_contrast(e): global auto_contrast_enabled auto_contrast_enabled = not auto_contrast_enabled button = document.getElementById("auto-contrast") button.setAttribute( "aria-pressed", "true" if auto_contrast_enabled else "false" ) button.innerText = ( "Max Contrast On" if auto_contrast_enabled else "Max Contrast" ) extract_slice() async def on_file_upload(e): file_list = e.target.files if file_list.length == 0: return first_file = file_list.item(0) file_ext = ( first_file.name.rsplit(".", 1)[-1].lower() if "." in first_file.name else "" ) global volume, volume_dims, file_type, voxel_spacing global slice_viewbox_dims, slice_viewbox_half_world try: loading_div = document.getElementById("loading") loading_div.innerText = "Loading file(s)..." loading_div.style.display = "block" document.getElementById("error").style.display = "none" voxel_spacing = np.array([1.0, 1.0, 1.0], dtype=float) if file_ext == "npz": bytes_data = await get_bytes_from_file(first_file) npz_file = np.load(BytesIO(bytes_data)) keys = list(npz_file.files) if len(keys) == 0: raise ValueError("No arrays found in the NPZ file.") data = npz_file[keys[0]] if data.ndim == 4: nZ, nY, nX = data.shape[:3] volume = data elif data.ndim == 3: nY, nX, nZ = data.shape volume = np.transpose(data, (2, 0, 1)) nZ, nY, nX = volume.shape else: raise ValueError( f"Expected a 3D or 4D array. Got shape: {data.shape}" ) volume_dims = (nZ, nY, nX) file_type = "NPZ" elif file_ext == "npy": bytes_data = await get_bytes_from_file(first_file) data = np.load(BytesIO(bytes_data)) if data.ndim == 4: nZ, nY, nX = data.shape[:3] volume = data elif data.ndim == 3: nY, nX, nZ = data.shape volume = np.transpose(data, (2, 0, 1)) nZ, nY, nX = volume.shape else: raise ValueError( f"Expected a 3D or 4D array. Got shape: {data.shape}" ) volume_dims = (nZ, nY, nX) file_type = "NPY" elif file_ext == "nrrd": bytes_data = await get_bytes_from_file(first_file) temp_file = tempfile.NamedTemporaryFile( suffix=".nrrd", delete=False ) temp_path = temp_file.name temp_file.write(bytes_data) temp_file.close() try: data, header = nrrd.read(temp_path) voxel_spacing = get_nrrd_spacing(header) data = np.rot90(data, k=-1, axes=(0, 2)) data = np.flip(data, axis=2) volume = data nZ, nY, nX = volume.shape volume_dims = (nZ, nY, nX) file_type = "NRRD" finally: os.unlink(temp_path) elif file_ext == "dcm": bytes_data = await get_bytes_from_file(first_file) dcm = pydicom.dcmread(BytesIO(bytes_data)) data = dcm.pixel_array if data.ndim == 4: nZ, nY, nX = data.shape[:3] volume = data elif data.ndim == 3: nZ, nY, nX = data.shape volume = data else: nY, nX = data.shape nZ = 1 volume = data.reshape(1, nY, nX) volume_dims = (nZ, nY, nX) file_type = "DCM" else: raise ValueError(f"Unsupported file type: {file_ext}") window.volume_dims = volume_dims slice_viewbox_dims = (volume_dims[2], volume_dims[1]) half_width_world = ((volume_dims[2] - 1) / 2.0) * float(voxel_spacing[0]) half_height_world = ((volume_dims[1] - 1) / 2.0) * float(voxel_spacing[1]) slice_viewbox_half_world = ( max(half_width_world, float(voxel_spacing[0]) / 2.0, 1.0), max(half_height_world, float(voxel_spacing[1]) / 2.0, 1.0), ) z_slider = document.getElementById("z-slider") z_slider.min = 0 z_slider.max = volume_dims[0] - 1 z_slider.value = volume_dims[0] // 2 x_slider = document.getElementById("x-slider") x_slider.min = 0 x_slider.max = volume_dims[2] - 1 x_slider.value = volume_dims[2] // 2 y_slider = document.getElementById("y-slider") y_slider.min = 0 y_slider.max = volume_dims[1] - 1 y_slider.value = volume_dims[1] // 2 for slider_id in ["angle-x-slider", "angle-y-slider", "angle-z-slider"]: document.getElementById(slider_id).value = 0 reset_view = getattr(window, "resetViewControls", None) if reset_view is not None: reset_view(False) reset_measurements = getattr(window, "resetMeasurements", None) if reset_measurements is not None: reset_measurements() extract_slice() document.getElementById("loading").style.display = "none" except Exception as err: document.getElementById("loading").style.display = "none" error_div = document.getElementById("error") error_div.innerHTML = f"Error loading file: {str(err)}" error_div.style.display = "block" async def get_bytes_from_file(file): array_buf = await file.arrayBuffer() return array_buf.to_bytes() add_event_listener( document.getElementById("file-upload"), "change", on_file_upload ) add_event_listener( document.getElementById("z-slider"), "input", lambda e: extract_slice(), ) add_event_listener( document.getElementById("x-slider"), "input", lambda e: extract_slice(), ) add_event_listener( document.getElementById("y-slider"), "input", lambda e: extract_slice(), ) add_event_listener( document.getElementById("angle-x-slider"), "input", lambda e: extract_slice(), ) add_event_listener( document.getElementById("angle-y-slider"), "input", lambda e: extract_slice(), ) add_event_listener( document.getElementById("angle-z-slider"), "input", lambda e: extract_slice(), ) add_event_listener( document.getElementById("auto-contrast"), "click", toggle_auto_contrast, ) window.extract_slice = extract_slice window.set_volume_axis_basis = set_volume_axis_basis window.reset_volume_axis_basis = reset_volume_axis_basis