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

Volume Slicer

Loading file(s)...
1.00x
0
0
0
Ultrasound slice
Click on the ultrasound to place up to two lines.
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) 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 extract_slice(): global volume, volume_dims, voxel_spacing 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) ax = math.radians(angle_x) ay = math.radians(angle_y) 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, ) R = Ry.dot(Rx) 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) spacing_x = float(voxel_spacing[0]) spacing_y = float(voxel_spacing[1]) spacing_z = float(voxel_spacing[2]) center_index = np.array( [(nX - 1) / 2.0, (nY - 1) / 2.0, (nZ - 1) / 2.0], dtype=float, ) center_world = np.array( [ center_index[0] * spacing_x, center_index[1] * spacing_y, center_index[2] * spacing_z, ], dtype=float, ) half_world = np.array( [ ((nX - 1) / 2.0) * spacing_x, ((nY - 1) / 2.0) * spacing_y, ((nZ - 1) / 2.0) * spacing_z, ], dtype=float, ) 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)) u_extent = max( abs(u_axis[0]) * half_world[0] + abs(u_axis[1]) * half_world[1] + abs(u_axis[2]) * half_world[2], 1.0, ) v_extent = max( abs(v_axis[0]) * half_world[0] + abs(v_axis[1]) * half_world[1] + abs(v_axis[2]) * half_world[2], 1.0, ) base_size = 256 if u_extent >= v_extent: out_w = base_size out_h = max(1, int(round(base_size * v_extent / u_extent))) else: out_h = base_size out_w = max(1, int(round(base_size * u_extent / v_extent))) 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_index = np.array([x_pos, y_pos, z_pos], dtype=float) origin_world = np.array( [ origin_index[0] * spacing_x, origin_index[1] * spacing_y, origin_index[2] * spacing_z, ], dtype=float, ) 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) pil_im = Image.fromarray(slice_img) if abs(angle_z) > 1e-9: fill_color = 0 if channels == 1 else tuple([0] * channels) pil_im = pil_im.rotate( -angle_z, resample=Image.Resampling.BILINEAR, expand=False, fillcolor=fill_color, ) 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}°" 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 try: document.getElementById("loading").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 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 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(), ) window.extract_slice = extract_slice