{ "cells": [ { "cell_type": "markdown", "id": "79e9884a", "metadata": {}, "source": [ "# Computer Tomography\n", "\n", "In this example, we use data from a *computer tomography* (CT) study of a cadaver head: " ] }, { "cell_type": "code", "execution_count": 1, "id": "94b5ef80", "metadata": { "execution": { "iopub.execute_input": "2025-05-13T11:30:31.941828Z", "iopub.status.busy": "2025-05-13T11:30:31.941728Z", "iopub.status.idle": "2025-05-13T11:30:32.355719Z", "shell.execute_reply": "2025-05-13T11:30:32.355253Z" }, "vscode": { "languageId": "plaintext" } }, "outputs": [], "source": [ "import libcarna" ] }, { "cell_type": "markdown", "id": "d9805aca", "metadata": {}, "source": [ "Get the data:" ] }, { "cell_type": "code", "execution_count": 2, "id": "1ee080d3", "metadata": { "execution": { "iopub.execute_input": "2025-05-13T11:30:32.357299Z", "iopub.status.busy": "2025-05-13T11:30:32.357117Z", "iopub.status.idle": "2025-05-13T11:30:32.484031Z", "shell.execute_reply": "2025-05-13T11:30:32.483644Z" }, "vscode": { "languageId": "plaintext" } }, "outputs": [ { "data": { "text/plain": [ "((256, 256, 99), dtype('uint16'))" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "data = libcarna.data.cthead()\n", "data.shape, data.dtype" ] }, { "cell_type": "markdown", "id": "4cd5ba6d", "metadata": {}, "source": [ "The data is 256 × 256 × 99 pixels (uint16).\n", "\n", "## Maximum Intensity Projection\n", "\n", "We rotate the head so that it stands upright:" ] }, { "cell_type": "code", "execution_count": 3, "id": "7af5a922", "metadata": { "execution": { "iopub.execute_input": "2025-05-13T11:30:32.485668Z", "iopub.status.busy": "2025-05-13T11:30:32.485559Z", "iopub.status.idle": "2025-05-13T11:30:32.940363Z", "shell.execute_reply": "2025-05-13T11:30:32.940013Z" }, "vscode": { "languageId": "plaintext" } }, "outputs": [ { "data": { "text/html": [ "\n", "
\n", "
\n", " \n", "
\n", " \n", "
\n", "
\n", "
\n", "\n", "
\n", "
\n", "
\n", " \n", "
\n", " 3272\n", " \n", "
\n", "\n", "
\n", " 2454\n", " \n", "
\n", "\n", "
\n", " 1636\n", " \n", "
\n", "\n", "
\n", " 818\n", " \n", "
\n", "\n", "
\n", " 0\n", " \n", "
\n", "
\n", "
\n", " \n", "
\n", "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "GEOMETRY_TYPE_VOLUME = 2\n", "\n", "# Create and configure frame renderer\n", "mip = libcarna.mip(GEOMETRY_TYPE_VOLUME, sr=400)\n", "r = libcarna.renderer(600, 450, [mip])\n", "\n", "# Create and configure scene\n", "root = libcarna.node()\n", "\n", "volume = libcarna.volume(\n", " GEOMETRY_TYPE_VOLUME,\n", " data,\n", " parent=root,\n", " spacing=(1, 1, 2),\n", ").rotate('x', 90).rotate('z', 90)\n", "\n", "camera = libcarna.camera(\n", " parent=root,\n", ").frustum(fov=90, z_near=10, z_far=1000).translate(z=300)\n", "\n", "# Render\n", "libcarna.imshow(r.render(camera), mip.cmap.bar(volume))" ] }, { "cell_type": "markdown", "id": "65f9cade", "metadata": {}, "source": [ "The spatial structure of the 3D image is difficult to perceive from that rendering.\n", "\n", "For a better visual perception, viewing the data from different angles is benefical, that can be achieved with\n", "as a subtle animation:" ] }, { "cell_type": "code", "execution_count": 4, "id": "493f1153", "metadata": { "execution": { "iopub.execute_input": "2025-05-13T11:30:32.942033Z", "iopub.status.busy": "2025-05-13T11:30:32.941917Z", "iopub.status.idle": "2025-05-13T11:30:33.347677Z", "shell.execute_reply": "2025-05-13T11:30:33.347213Z" }, "vscode": { "languageId": "plaintext" } }, "outputs": [ { "data": { "text/html": [ "\n", "
\n", "
\n", " \n", "
\n", " \n", "
\n", "
\n", "
\n", "\n", "
\n", "
\n", "
\n", " \n", "
\n", " 3272\n", " \n", "
\n", "\n", "
\n", " 2454\n", " \n", "
\n", "\n", "
\n", " 1636\n", " \n", "
\n", "\n", "
\n", " 818\n", " \n", "
\n", "\n", "
\n", " 0\n", " \n", "
\n", "
\n", "
\n", " \n", "
\n", "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Render as animation\n", "libcarna.imshow(\n", " libcarna.animate(\n", " libcarna.animate.rotate_local(camera),\n", " n_frames=100,\n", " ).render(r, camera),\n", " mip.cmap.bar(volume),\n", ")" ] }, { "cell_type": "markdown", "id": "db245a72", "metadata": {}, "source": [ "## Hounsfield Unit Normalization\n", "\n", "The data from CT scanners usually comes in *Hounsfield Units* (HU), that range from -1024 to +3071. In the HU scale,\n", "air roughly corresponds to -1000 HU, water to 0 HU, and bone tissue to +1000 HU. The image intensities in this dataset\n", "are not normalized to the HU scale.\n", "\n", "Normalization of CT data to the HU scale is beneficial, because it permits direct identification of air, water-rich\n", "tissue, and bone tissue. With HU-normalized images, we can also render *Digitally Reconstructed Radiographs* (DRR), as \n", "shown [further below](#Digitally-Reconstructed-Radiographs).\n", "\n", "LibCarna-Python provides a heuristic method for *approximative* normalization of CT data to the HU scale, that is based\n", "on the histogram of the intensisty values:" ] }, { "cell_type": "code", "execution_count": 5, "id": "696c040a", "metadata": { "execution": { "iopub.execute_input": "2025-05-13T11:30:33.350375Z", "iopub.status.busy": "2025-05-13T11:30:33.350168Z", "iopub.status.idle": "2025-05-13T11:30:33.441500Z", "shell.execute_reply": "2025-05-13T11:30:33.440988Z" }, "vscode": { "languageId": "plaintext" } }, "outputs": [], "source": [ "data_hu = libcarna.normalize_hounsfield_units(data)" ] }, { "cell_type": "markdown", "id": "ab1a5633", "metadata": {}, "source": [ "We then define a renderable volume with the HU-normalized image intensities:" ] }, { "cell_type": "code", "execution_count": 6, "id": "fa026447", "metadata": { "execution": { "iopub.execute_input": "2025-05-13T11:30:33.443055Z", "iopub.status.busy": "2025-05-13T11:30:33.442941Z", "iopub.status.idle": "2025-05-13T11:30:34.253172Z", "shell.execute_reply": "2025-05-13T11:30:34.252573Z" }, "vscode": { "languageId": "plaintext" } }, "outputs": [], "source": [ "GEOMETRY_TYPE_HU_VOLUME = 3\n", "\n", "hu_volume = libcarna.volume(\n", " GEOMETRY_TYPE_HU_VOLUME,\n", " data_hu,\n", " units='hu',\n", " parent=root,\n", " spacing=volume.spacing,\n", " local_transform=volume.local_transform,\n", " normals=True,\n", ")" ] }, { "cell_type": "markdown", "id": "b6866626", "metadata": {}, "source": [ "Note that `units='hu'` is needed to correctly interpret the intensities in `data_hu`. We also employ `normals=True` to\n", "pre-compute the normal vectors of the data (see below).\n", "\n", "## Direct Volume Rendering\n", "\n", "In a *Direct Volume Rendering* (DVR), surfaces are rendered by simulation of the absorption of light. This simulation\n", "is most realstic, when the spatial orientation of the surfaces can be taken into account, which requires that the\n", "normals of the volume have been computed (this is why we used `normals=True` when we created the `volume` node).\n", "\n", "We use a ramp function to strip out the air from the visualization, and we use the `hu_volume.normalized` auxiliary\n", "function to directly supply the HU values for the ramp function:" ] }, { "cell_type": "code", "execution_count": 7, "id": "6e7f236c", "metadata": { "execution": { "iopub.execute_input": "2025-05-13T11:30:34.254808Z", "iopub.status.busy": "2025-05-13T11:30:34.254696Z", "iopub.status.idle": "2025-05-13T11:30:35.014910Z", "shell.execute_reply": "2025-05-13T11:30:35.014452Z" }, "vscode": { "languageId": "plaintext" } }, "outputs": [ { "data": { "text/html": [ "\n", "
\n", "
\n", " \n", "
\n", " \n", "
\n", "
\n", "
\n", "\n", "
\n", "
\n", "
\n", " \n", "
\n", " 3.1k\n", " \n", "
\n", "\n", "
\n", " 2.0k\n", " \n", "
\n", "\n", "
\n", " 1.0k\n", " \n", "
\n", "\n", "
\n", " 0\n", " \n", "
\n", "\n", "
\n", " -1.0k\n", " \n", "
\n", "
\n", "
\n", " \n", "
\n", "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dvr = libcarna.dvr(\n", " GEOMETRY_TYPE_HU_VOLUME, sr=800, transl=1, diffuse=0.8,\n", ")\n", "dvr.cmap('BrBG', ramp=hu_volume.normalized((0, 100)))\n", "\n", "libcarna.imshow(\n", " libcarna.animate(\n", " libcarna.animate.rotate_local(camera),\n", " n_frames=100,\n", " ).render(\n", " libcarna.renderer(600, 450, [dvr]),\n", " camera,\n", " ),\n", " dvr.cmap.bar(hu_volume),\n", ")" ] }, { "cell_type": "markdown", "id": "ee0f8920", "metadata": {}, "source": [ "## Digitally Reconstructed Radiographs\n", "\n", "*Digitally Reconstructed Radiographs* (DRRs) are 2D images created from 3D data, like CT scans, to simulate what a real\n", "X-ray image would look like:" ] }, { "cell_type": "code", "execution_count": 8, "id": "41f16aea", "metadata": { "execution": { "iopub.execute_input": "2025-05-13T11:30:35.016862Z", "iopub.status.busy": "2025-05-13T11:30:35.016752Z", "iopub.status.idle": "2025-05-13T11:30:35.522063Z", "shell.execute_reply": "2025-05-13T11:30:35.521595Z" }, "vscode": { "languageId": "plaintext" } }, "outputs": [ { "data": { "text/html": [ "\n", "
\n", "
\n", " \n", "
\n", " \n", "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "libcarna.imshow(\n", " libcarna.animate(\n", " libcarna.animate.rotate_local(camera),\n", " n_frames=100,\n", " ).render(\n", " libcarna.renderer(\n", " 600, 450, [\n", " libcarna.drr(\n", " GEOMETRY_TYPE_HU_VOLUME, sr=800, inverse=True,\n", " )\n", " ],\n", " bgcolor=libcarna.color.WHITE_NO_ALPHA,\n", " ),\n", " camera,\n", " ),\n", ")" ] } ], "metadata": { "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.10" } }, "nbformat": 4, "nbformat_minor": 5 }