{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Reconstructing DICOM Data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import numpy as np\n",
    "import torch\n",
    "import pytomography\n",
    "from pytomography.io.SPECT import dicom\n",
    "from pytomography.transforms import SPECTAttenuationTransform, SPECTPSFTransform\n",
    "from pytomography.algorithms import OSEMOSL\n",
    "from pytomography.projections import SPECTSystemMatrix\n",
    "import matplotlib.pyplot as plt\n",
    "import torch\n",
    "import pydicom\n",
    "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
    "pytomography.device = device"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "First we specify the required filepaths to open the SPECT scan (`file_NM`). Note that we open the CT data (`files_CT`) (in units of Hounsfield units) *and* an attenuation map (`file_AM`) provided by the vendor. Only one is needed for reconstruction (PyTomography has functionality for converting CT images in units of HU to attenuation maps in units of $\\text{cm}^{-1}$). We will compare reconstruction using the vendor attenuation map and the attenuation map created using PyTomography."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "# initialize the `path`` variable below to specify the location of the required data\n",
    "path = '/disk1/pytomography_tutorial_data/dicom_tutorial/' \n",
    "path_CT = os.path.join(path, 'CT_files')\n",
    "files_CT = [os.path.join(path_CT, file) for file in os.listdir(path_CT)]\n",
    "file_AM = os.path.join(path, 'attenuation_map.dcm')\n",
    "file_NM = os.path.join(path, 'projection_data.dcm')"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The object meta, image meta, and photopeak can be obtained using the `dicom_projections_to_data` function. Note that most raw SPECT DICOM files contain data from multiple energy windows. If the `index_peak` argument isn't included, then projections the third argument it returns would contain all energy windows (along the batch dimension). In this case, we specify `index_peak=3`, so it only returns the projections corresponding to that energy window. This infomation can be obtained by opening the dicom file using pydicom and looking at the `EnergyWindowInformationSequence` attribute"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "object_meta, image_meta, photopeak = dicom.get_projections(file_NM, index_peak=3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<matplotlib.colorbar.Colorbar at 0x7f66ccd77d90>"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 24016 characters]",
      "text/plain": [
       "<Figure size 700x600 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "plt.figure(figsize=(7,6))\n",
    "plt.pcolormesh(photopeak[0,0].cpu().T, cmap='nipy_spectral')\n",
    "plt.axis('off')\n",
    "plt.colorbar(label='Counts')"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Scatter projections can be estimated using the triple energy window function `dicom_get_scatterTEW`. One needs to specify the index of the peak, and lower/upper energy windows"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "scatter = dicom.get_scatter_from_TEW(file_NM, index_peak=3, index_lower=2, index_upper=4)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The attenuation map can be obtained in two ways\n",
    "\n",
    "1. If you have access to an attenuation map from the manufacturer in a single DICOM file, you should use this. This is obtained using the `get_attenuation_map_from_file`\n",
    "2. If you don't have access to this attenuation map, then you're not out of luck. PyTomography has functionality to arrange, align, resize, and rescale a sequence of CT scans into an attenuation map. It  uses an approximate bilinear relationship between Hounsfield Units and linear attenuation coefficients. You can use the `get_attenuation_map_from_CT_slices` function, and give as input the files corresponding to the CT scan, the raw projection file name, and the index in the raw projection file corresponding to the photopeak."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Cortical Bone Peak: 1419.3499755859375 HU\n",
      "Effective CT Energy Determined: 75.89505539164563 keV\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/home/gpuvmadm/PyTomography/src/pytomography/io/io_utils.py:43: RuntimeWarning: overflow encountered in exp\n",
      "  return c1*np.exp(-d1*np.sqrt(energy)) + c2*np.exp(-d2*np.sqrt(energy))\n"
     ]
    }
   ],
   "source": [
    "attenuation_map_from_file = dicom.get_attenuation_map_from_file(file_AM)\n",
    "attenuation_map_from_CT_slices = dicom.get_attenuation_map_from_CT_slices(files_CT, file_NM, index_peak=3)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can compare the vendor obtained attenuation map and the approximated one below:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "sample_slice_from_file = attenuation_map_from_file.cpu()[0][:,70].T\n",
    "sample_slice_from_CT_slices = attenuation_map_from_CT_slices.cpu()[0][:,70].T\n",
    "ratio = (sample_slice_from_file+1e-3)/(sample_slice_from_CT_slices+1e-3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<matplotlib.colorbar.Colorbar at 0x7f66c8ad96d0>"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 60020 characters]",
      "text/plain": [
       "<Figure size 1000x300 with 6 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "plt.subplots(1,3,figsize=(10,3))\n",
    "plt.subplot(131)\n",
    "plt.title('From File')\n",
    "plt.pcolormesh(sample_slice_from_file, cmap='nipy_spectral')\n",
    "plt.colorbar()\n",
    "plt.subplot(132)\n",
    "plt.title('From CT Slices')\n",
    "plt.pcolormesh(sample_slice_from_CT_slices, cmap='nipy_spectral')\n",
    "plt.colorbar()\n",
    "plt.subplot(133)\n",
    "plt.title('Ratio')\n",
    "plt.pcolormesh(ratio, cmap='seismic', vmin=0.9, vmax=1.1)\n",
    "plt.colorbar()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "While the two maps are very similar, they differ by approximately $\\pm 5\\%$ in highly attenuating regions. This minor difference can have significant implications on the reconstruction (as we shall see shortly)."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can obtain the required parameters for PSF modeling provided we know the name of the scanner (`scanner_model`), the type of collimators used (`collimator_name`) and the energy (`energy_kev`) for finding the linear attenuation coefficient of lead. In this case, we know the radioactive substance used was Lu177, so we set the energy to the energy of the photopeak: 208keV."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Function:         sigma_fit : function = lambda r, a, b: a*r+b,\n",
      "\n",
      "Parameters: (0.03161992134409504, 0.12485030464233879)\n",
      "Dimensions: 2D\n",
      "Maximum sigmas: 3\n"
     ]
    }
   ],
   "source": [
    "camera_model = 'SymbiaTSeries'\n",
    "collimator_name = 'ME'\n",
    "energy_kev = 208\n",
    "psf_meta = dicom.get_psfmeta_from_scanner_params(camera_model, collimator_name, energy_kev)\n",
    "print(psf_meta)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Reconstruct the object"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We're now ready to reconstruct the DICOM object. We'll try using attenuation correction with both the vendors attenuation map and the one created by PyTomography\n",
    "\n",
    "So we'll create a function that performs reconstruction given a particular attenuation map:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "def reconstruct_phantom(attenuation_map):\n",
    "    # Create transforms\n",
    "    att_transform = SPECTAttenuationTransform(attenuation_map)\n",
    "    psf_transform = SPECTPSFTransform(psf_meta)\n",
    "    # Create system matrix\n",
    "    system_matrix = SPECTSystemMatrix(\n",
    "        obj2obj_transforms = [att_transform,psf_transform],\n",
    "        im2im_transforms = [],\n",
    "        object_meta = object_meta,\n",
    "        image_meta = image_meta)\n",
    "    # Initialize reconstruction algorithm\n",
    "    reconstruction_algorithm = OSEMOSL(\n",
    "        image = photopeak,\n",
    "        system_matrix = system_matrix,\n",
    "        scatter=scatter)\n",
    "    # Reconstruct object\n",
    "    reconstructed_object = reconstruction_algorithm(n_iters=4, n_subsets=10)\n",
    "    return reconstructed_object"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "reconstructed_object_atten_from_file = reconstruct_phantom(attenuation_map_from_file)\n",
    "reconstructed_object_atten_from_CT_slices = reconstruct_phantom(attenuation_map_from_CT_slices)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We'll compare these reconstructions to one done on the vendors software, which also performed OSEM with 4 iterations, 10 subsets, with attenuation modeling, PSF modeling, and triple energy window scatter correction."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "ds_recon = pydicom.read_file(os.path.join(path, 'scanner_recon.dcm'))\n",
    "reconstructed_object_vendor = ds_recon.pixel_array / 90\n",
    "reconstructed_object_vendor = np.transpose(reconstructed_object_vendor, (2,1,0))"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Plotting"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can looks at axial slices from all three reconstructions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "slice_atten_from_file = reconstructed_object_atten_from_file.cpu()[0][:,:,70].T\n",
    "slice_atten_from_CT_slices = reconstructed_object_atten_from_CT_slices.cpu()[0][:,:,70].T\n",
    "slice_vendor = reconstructed_object_vendor[:,:,70].T"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 24452 characters]",
      "text/plain": [
       "<Figure size 1000x300 with 6 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "plt.subplots(1,3,figsize=(10,3))\n",
    "plt.subplot(131)\n",
    "plt.title('From File')\n",
    "plt.pcolormesh(slice_atten_from_file , cmap='nipy_spectral', vmax=110)\n",
    "plt.axis('off')\n",
    "plt.colorbar()\n",
    "plt.subplot(132)\n",
    "plt.title('From CT Slices')\n",
    "plt.pcolormesh(slice_atten_from_CT_slices, cmap='nipy_spectral', vmax=110)\n",
    "plt.axis('off')\n",
    "plt.colorbar()\n",
    "plt.subplot(133)\n",
    "plt.title('From Vendor')\n",
    "plt.pcolormesh(slice_vendor, cmap='nipy_spectral', vmax=110)\n",
    "plt.axis('off')\n",
    "plt.colorbar()\n",
    "plt.show()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Careful observation of the top bright lobe shows that the reconstruction obtained by using the vendors attenuation map gives a reconstruction more consistent with the vendor than that using seperate CT slices."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "torch",
   "language": "python",
   "name": "python3"
  },
  "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.9.16"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
