{
  "id": 524316,
  "title": "3D Point Cloud or Voxel Grid Loading -- Optimizations",
  "url": "/competitions/rsna-2024-lumbar-spine-degenerative-classification/discussion/524316",
  "author_name": "Victor S",
  "post_date": "2024-08-05T16:13:25.877000",
  "votes": 3,
  "comment_count": 6,
  "views": 0,
  "content": "<p>Here is my attempt at loading series in 3D with the appropriate slice transforms: <a href=\"https://www.kaggle.com/code/vsahin/rsna-2024-dicom-3d-loading\" target=\"_blank\">https://www.kaggle.com/code/vsahin/rsna-2024-dicom-3d-loading</a></p>\n<p>The downside is, it is <em>extremely</em> slow and not really feasible for actually loading the dataset. So would really appreciate speedup tips.</p>\n<pre><code>def (dir_path):\n    pcds_xyz = []\n    pcds_d = []\n\n    paths = glob.(os.path.(dir_path, ))\n    for path in paths:\n        dicom_slice = pydicom.(path)\n        img = np.(dicom_slice.pixel_array, -)\n        pcd = o3d.geometry.()\n        x, y, z = np.(img)\n\n        index_voxel = np.((x, y, z))\n        grid_index_array = index_voxel.T\n        pcd.points = o3d.utility.(grid_index_array)\n\n        vals = np.([img[x, y, z] for x, y, z in grid_index_array])\n\n        dX, dY = dicom_slice.PixelSpacing\n        X = np.((dicom_slice.ImageOrientationPatient[:]) + []) * dX\n        Y = np.((dicom_slice.ImageOrientationPatient[:]) + []) * dY\n        S = np.((dicom_slice.ImagePositionPatient) + [])\n\n        transform_matrix = np.([X, Y, np.((X)), S]).T\n        transformed_pcd = pcd.(transform_matrix)\n\n        pcds_xyz.(transformed_pcd.points)\n        pcds_d.(vals)\n\n    pcd_xyzd = np.((pcds_xyz, np.(pcds_d, -)))\n    return pcd_xyzd\n</code></pre>\n<pre><code>def read_series_as_voxel_grid(dir_path):\n    pcd_xyzd = read_series_as_pcd(dir_path)\n\n    pcd_overall = o3d.geometry.PointCloud()\n    pcd_overall. = o3d.utility.Vector3dVector(pcd_xyzd[:, :])\n    pcd_overall.colors = o3d.utility.Vector3dVector(.repeat(.expand_dims(pcd_xyzd[:, ], -), , -).astype(.int32))\n\n    paths = glob.glob(os.path.(dir_path, ))\n    dicom_slice = pydicom.read_file(paths[])\n    dX, dY = dicom_slice.PixelSpacing\n\n    voxel_grid = o3d.geometry.VoxelGrid().create_from_point_cloud(pcd_overall, dX)\n\n    coords = .([voxel.grid_index  voxel  voxel_grid.get_voxels()])\n    vals = .([voxel.[]  voxel  voxel_grid.get_voxels()])\n\n    pcd_overall.clear()\n    voxel_grid.clear()\n\n    size = .(coords, axis=) + \n     = .zeros((size[], size[], size[]))\n\n     index,   enumerate(coords):\n        [([], [], [])] = vals[index]\n\n    f = pgzip.PgzipFile(cache_path, )\n    .(f, )\n    f.()\n\n     pcd_overall\n     voxel_grid\n\n     \n</code></pre>",
  "messages": [
    {
      "id": 2948031,
      "postDate": "2024-08-05T16:13:25.877Z",
      "content": "<p>Here is my attempt at loading series in 3D with the appropriate slice transforms: <a href=\"https://www.kaggle.com/code/vsahin/rsna-2024-dicom-3d-loading\" target=\"_blank\">https://www.kaggle.com/code/vsahin/rsna-2024-dicom-3d-loading</a></p>\n<p>The downside is, it is <em>extremely</em> slow and not really feasible for actually loading the dataset. So would really appreciate speedup tips.</p>\n<pre><code>def (dir_path):\n    pcds_xyz = []\n    pcds_d = []\n\n    paths = glob.(os.path.(dir_path, ))\n    for path in paths:\n        dicom_slice = pydicom.(path)\n        img = np.(dicom_slice.pixel_array, -)\n        pcd = o3d.geometry.()\n        x, y, z = np.(img)\n\n        index_voxel = np.((x, y, z))\n        grid_index_array = index_voxel.T\n        pcd.points = o3d.utility.(grid_index_array)\n\n        vals = np.([img[x, y, z] for x, y, z in grid_index_array])\n\n        dX, dY = dicom_slice.PixelSpacing\n        X = np.((dicom_slice.ImageOrientationPatient[:]) + []) * dX\n        Y = np.((dicom_slice.ImageOrientationPatient[:]) + []) * dY\n        S = np.((dicom_slice.ImagePositionPatient) + [])\n\n        transform_matrix = np.([X, Y, np.((X)), S]).T\n        transformed_pcd = pcd.(transform_matrix)\n\n        pcds_xyz.(transformed_pcd.points)\n        pcds_d.(vals)\n\n    pcd_xyzd = np.((pcds_xyz, np.(pcds_d, -)))\n    return pcd_xyzd\n</code></pre>\n<pre><code>def read_series_as_voxel_grid(dir_path):\n    pcd_xyzd = read_series_as_pcd(dir_path)\n\n    pcd_overall = o3d.geometry.PointCloud()\n    pcd_overall. = o3d.utility.Vector3dVector(pcd_xyzd[:, :])\n    pcd_overall.colors = o3d.utility.Vector3dVector(.repeat(.expand_dims(pcd_xyzd[:, ], -), , -).astype(.int32))\n\n    paths = glob.glob(os.path.(dir_path, ))\n    dicom_slice = pydicom.read_file(paths[])\n    dX, dY = dicom_slice.PixelSpacing\n\n    voxel_grid = o3d.geometry.VoxelGrid().create_from_point_cloud(pcd_overall, dX)\n\n    coords = .([voxel.grid_index  voxel  voxel_grid.get_voxels()])\n    vals = .([voxel.[]  voxel  voxel_grid.get_voxels()])\n\n    pcd_overall.clear()\n    voxel_grid.clear()\n\n    size = .(coords, axis=) + \n     = .zeros((size[], size[], size[]))\n\n     index,   enumerate(coords):\n        [([], [], [])] = vals[index]\n\n    f = pgzip.PgzipFile(cache_path, )\n    .(f, )\n    f.()\n\n     pcd_overall\n     voxel_grid\n\n     \n</code></pre>",
      "rawMarkdown": "Here is my attempt at loading series in 3D with the appropriate slice transforms: https://www.kaggle.com/code/vsahin/rsna-2024-dicom-3d-loading\n\nThe downside is, it is *extremely* slow and not really feasible for actually loading the dataset. So would really appreciate speedup tips.\n\n```\ndef read_series_as_pcd(dir_path):\n    pcds_xyz = []\n    pcds_d = []\n\n    paths = glob.glob(os.path.join(dir_path, \"*.dcm\"))\n    for path in paths:\n        dicom_slice = pydicom.read_file(path)\n        img = np.expand_dims(dicom_slice.pixel_array, -1)\n        pcd = o3d.geometry.PointCloud()\n        x, y, z = np.where(img)\n\n        index_voxel = np.vstack((x, y, z))\n        grid_index_array = index_voxel.T\n        pcd.points = o3d.utility.Vector3dVector(grid_index_array)\n\n        vals = np.array([img[x, y, z] for x, y, z in grid_index_array])\n\n        dX, dY = dicom_slice.PixelSpacing\n        X = np.array(list(dicom_slice.ImageOrientationPatient[:3]) + [0]) * dX\n        Y = np.array(list(dicom_slice.ImageOrientationPatient[3:]) + [0]) * dY\n        S = np.array(list(dicom_slice.ImagePositionPatient) + [1])\n\n        transform_matrix = np.array([X, Y, np.zeros(len(X)), S]).T\n        transformed_pcd = pcd.transform(transform_matrix)\n\n        pcds_xyz.extend(transformed_pcd.points)\n        pcds_d.extend(vals)\n    \n    pcd_xyzd = np.hstack((pcds_xyz, np.expand_dims(pcds_d, -1)))\n    return pcd_xyzd\n\n```\n\n```\ndef read_series_as_voxel_grid(dir_path):\n    pcd_xyzd = read_series_as_pcd(dir_path)\n\n    pcd_overall = o3d.geometry.PointCloud()\n    pcd_overall.points = o3d.utility.Vector3dVector(pcd_xyzd[:, :3])\n    pcd_overall.colors = o3d.utility.Vector3dVector(np.repeat(np.expand_dims(pcd_xyzd[:, 3], -1), 3, -1).astype(np.int32))\n\n    paths = glob.glob(os.path.join(dir_path, \"*.dcm\"))\n    dicom_slice = pydicom.read_file(paths[0])\n    dX, dY = dicom_slice.PixelSpacing\n\n    voxel_grid = o3d.geometry.VoxelGrid().create_from_point_cloud(pcd_overall, dX)\n\n    coords = np.array([voxel.grid_index for voxel in voxel_grid.get_voxels()])\n    vals = np.array([voxel.color[0] for voxel in voxel_grid.get_voxels()])\n\n    pcd_overall.clear()\n    voxel_grid.clear()\n\n    size = np.max(coords, axis=0) + 1\n    grid = np.zeros((size[0], size[1], size[2]))\n\n    for index, coord in enumerate(coords):\n        grid[(coord[0], coord[1], coord[2])] = vals[index]\n\n    f = pgzip.PgzipFile(cache_path, \"w\")\n    np.save(f, grid)\n    f.close()\n\n    del pcd_overall\n    del voxel_grid\n\n    return grid\n```",
      "votes": 3
    },
    {
      "id": 2948844,
      "postDate": "2024-08-06T07:00:38.477Z",
      "content": "<p>Your idea is great. I have been trying to do something like this by rotating 3D NumPy volumes with padding around it. It was not very successful.<br>\n<strong>2 questions on my end:</strong></p>\n<ol>\n<li>What happens when <code>dicom_slice.SliceThickness</code> is not an int.</li>\n<li>I tried visualizing the <code>grid</code> volume returned by DICOM series (study_id=46494080, series_id=1763376930) with <code>read_series_as_voxel_grid</code>, one of the slices looked like this:<br>\n<img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F1303569%2Fa43e29a490fd17c71c67d2a5edcbe99c%2FScreenshot%202024-08-06%20122715.png?generation=1722927491776227&amp;alt=media\" alt=\"\"></li>\n</ol>\n<p>I think the issue is in the last part: <code>grid[(coord[1], coord[0], coord[2])] = vals[index]</code>. May be these indices don't line up with <code>pcd_xyzd</code></p>",
      "rawMarkdown": "Your idea is great. I have been trying to do something like this by rotating 3D NumPy volumes with padding around it. It was not very successful.\n**2 questions on my end:**\n1. What happens when `dicom_slice.SliceThickness` is not an int.\n1. I tried visualizing the `grid` volume returned by DICOM series (study_id=46494080, series_id=1763376930) with `read_series_as_voxel_grid`, one of the slices looked like this:\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F1303569%2Fa43e29a490fd17c71c67d2a5edcbe99c%2FScreenshot%202024-08-06%20122715.png?generation=1722927491776227&alt=media)\n\nI think the issue is in the last part: `grid[(coord[1], coord[0], coord[2])] = vals[index]`. May be these indices don't line up with `pcd_xyzd`",
      "votes": 1,
      "replies": [
        {
          "id": 2949274,
          "postDate": "2024-08-06T14:38:56.210Z",
          "content": "<p>On the first question, my bad for leaving that bit in. Slice thickness is not relevant at all.</p>\n<p>On the second question, great catch! Also my bad for not checking. They indeed do not align with <code>pcd_xyzd</code>, just added the bugfix.<br>\n<img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F1734709%2F892ccd20d46ca8d6e1c67639610ae6d2%2F__results___7_0.png?generation=1722955580921247&amp;alt=media\" alt=\"\"></p>",
          "rawMarkdown": "On the first question, my bad for leaving that bit in. Slice thickness is not relevant at all.\n\nOn the second question, great catch! Also my bad for not checking. They indeed do not align with `pcd_xyzd`, just added the bugfix.\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F1734709%2F892ccd20d46ca8d6e1c67639610ae6d2%2F__results___7_0.png?generation=1722955580921247&alt=media)",
          "votes": 1
        }
      ]
    },
    {
      "id": 2949390,
      "postDate": "2024-08-06T16:04:39.863Z",
      "content": "<p><a href=\"https://www.kaggle.com/vsahin\" target=\"_blank\">@vsahin</a> Great, I was also about to suggest not repeating <code>pixel_array</code> to reduce computational time along with using colors to store value. But you already did that. So here is a minor improvement:</p>\n<pre><code> ():\n    pcd_overall = o3d.geometry.PointCloud()\n\n     path  Path(dir_path).glob():\n        dicom_slice = dcmread(path)\n        img = np.expand_dims(dicom_slice.pixel_array, -)\n        x, y, z = np.where(img)\n\n        index_voxel = np.vstack((x, y, z))\n        grid_index_array = index_voxel.T\n        pcd = o3d.geometry.PointCloud(o3d.utility.Vector3dVector(grid_index_array))\n\n        vals = np.repeat(np.expand_dims(img[x, y, z], -), , -)\n        pcd.colors = o3d.utility.Vector3dVector(vals) \n\n        dX, dY = dicom_slice.PixelSpacing\n        X = np.array((dicom_slice.ImageOrientationPatient[:]) + []) * dX\n        Y = np.array((dicom_slice.ImageOrientationPatient[:]) + []) * dY\n        S = np.array((dicom_slice.ImagePositionPatient) + [])\n\n        transform_matrix = np.array([X, Y, np.zeros((X)), S]).T\n        pcd_overall += pcd.transform(transform_matrix)\n\n     pcd_overall\n\n ():\n    pcd_overall = read_series_as_pcd(dir_path)\n\n    path = (Path(dir_path).glob())\n    dicom_slice = dcmread(path)\n    dX, dY = dicom_slice.PixelSpacing\n\n    voxel_grid = o3d.geometry.VoxelGrid().create_from_point_cloud(pcd_overall, dX)\n\n    coords = np.array([voxel.grid_index  voxel  voxel_grid.get_voxels()])\n    vals = np.array([voxel.color[]  voxel  voxel_grid.get_voxels()])\n\n    size = np.(coords, axis=) + \n    grid = np.zeros((size[], size[], size[]))\n\n    grid[coords[:, ], coords[:, ], coords[:, ]] = vals\n\n     grid\n</code></pre>\n<p>This directly creates <code>pcd_overall</code> by summing PCDs: <code>pcd_overall += pcd.transform(transform_matrix)</code><br>\nAlso, removing loops in favor of NumPy indexing: <code>img[x, y, z]</code> and <code>grid[coords[:, 0], coords[:, 1], coords[:, 2]] = vals</code></p>\n<p>Your new code's <code>timeit</code> for <code>read_series_as_voxel_grid(train_images_path / \"46494080\" / \"1763376930\")</code>: </p>\n<pre><code>. s ±  ms per loop (mean ± std. dev. of  runs,  loop each)\n</code></pre>\n<p>My above modified code:</p>\n<pre><code>. s ±  ms per loop (mean ± std. dev. of  runs,  loop each)\n</code></pre>",
      "rawMarkdown": "@vsahin Great, I was also about to suggest not repeating `pixel_array` to reduce computational time along with using colors to store value. But you already did that. So here is a minor improvement:\n```python\ndef read_series_as_pcd(dir_path):\n    pcd_overall = o3d.geometry.PointCloud()\n\n    for path in Path(dir_path).glob(\"*.dcm\"):\n        dicom_slice = dcmread(path)\n        img = np.expand_dims(dicom_slice.pixel_array, -1)\n        x, y, z = np.where(img)\n\n        index_voxel = np.vstack((x, y, z))\n        grid_index_array = index_voxel.T\n        pcd = o3d.geometry.PointCloud(o3d.utility.Vector3dVector(grid_index_array))\n\n        vals = np.repeat(np.expand_dims(img[x, y, z], -1), 3, -1)\n        pcd.colors = o3d.utility.Vector3dVector(vals) # Converted to np.float64\n\n        dX, dY = dicom_slice.PixelSpacing\n        X = np.array(list(dicom_slice.ImageOrientationPatient[:3]) + [0]) * dX\n        Y = np.array(list(dicom_slice.ImageOrientationPatient[3:]) + [0]) * dY\n        S = np.array(list(dicom_slice.ImagePositionPatient) + [1])\n\n        transform_matrix = np.array([X, Y, np.zeros(len(X)), S]).T\n        pcd_overall += pcd.transform(transform_matrix)\n\n    return pcd_overall\n\ndef read_series_as_voxel_grid(dir_path):\n    pcd_overall = read_series_as_pcd(dir_path)\n\n    path = next(Path(dir_path).glob(\"*.dcm\"))\n    dicom_slice = dcmread(path)\n    dX, dY = dicom_slice.PixelSpacing\n\n    voxel_grid = o3d.geometry.VoxelGrid().create_from_point_cloud(pcd_overall, dX)\n\n    coords = np.array([voxel.grid_index for voxel in voxel_grid.get_voxels()])\n    vals = np.array([voxel.color[0] for voxel in voxel_grid.get_voxels()])\n\n    size = np.max(coords, axis=0) + 1\n    grid = np.zeros((size[0], size[1], size[2]))\n\n    grid[coords[:, 0], coords[:, 1], coords[:, 2]] = vals\n\n    return grid\n```\nThis directly creates `pcd_overall` by summing PCDs: `pcd_overall += pcd.transform(transform_matrix)`\nAlso, removing loops in favor of NumPy indexing: `img[x, y, z]` and `grid[coords[:, 0], coords[:, 1], coords[:, 2]] = vals`\n\nYour new code's `timeit` for `read_series_as_voxel_grid(train_images_path / \"46494080\" / \"1763376930\")`: \n```\n15.4 s ± 363 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n```\nMy above modified code:\n```\n9.05 s ± 173 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n```",
      "votes": 2,
      "replies": [
        {
          "id": 2949445,
          "postDate": "2024-08-06T16:36:05.820Z",
          "content": "<p>Looks great! Next thing I am wondering is if we could get some speedup from CUDA for the point cloud transformation part <br>\n<a href=\"https://docs.cupy.dev/en/stable/reference/generated/cupyx.scipy.ndimage.affine_transform.html\" target=\"_blank\">https://docs.cupy.dev/en/stable/reference/generated/cupyx.scipy.ndimage.affine_transform.html</a></p>",
          "rawMarkdown": "Looks great! Next thing I am wondering is if we could get some speedup from CUDA for the point cloud transformation part \nhttps://docs.cupy.dev/en/stable/reference/generated/cupyx.scipy.ndimage.affine_transform.html",
          "votes": 1,
          "replies": [
            {
              "id": 2950417,
              "postDate": "2024-08-07T14:48:07.043Z",
              "content": "<p>When you have GPU turned on, all legacy API objects created by <code>open3d</code> are on the GPU:</p>\n<pre><code>((o3d.geometry.PointCloud()), (o3d.geometry.VoxelGrid()))\n</code></pre>\n<p>Prints: <code>&lt;class 'open3d.cuda.pybind.geometry.PointCloud'&gt; &lt;class 'open3d.cuda.pybind.geometry.VoxelGrid'&gt;</code></p>\n<p>I tried the new <code>o3d.t.geometry.PointCloud()</code> (there is no equivalent for <code>VoxelGrid</code> in the new API):</p>\n<ul>\n<li>Appending 2 <code>PointCloud</code> objects resulted in error for some unknown reason.</li>\n<li>It did save 2 seconds: <code>7.05 s ± 119 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)</code>. Maybe it was because I used <code>float32</code><br>\nOnly <code>read_series_as_pcd</code> need to be modified from above:</li>\n</ul>\n<pre><code> ():\n    device = o3d.core.Device()\n    dtype = o3d.core.float32\n    pcd_overall = o3d.geometry.PointCloud()\n\n     path  Path(dir_path).glob():\n        dicom_slice = dcmread(path)\n        img = np.expand_dims(dicom_slice.pixel_array, -)\n        x, y, z = np.where(img)\n\n        index_voxel = np.vstack((x, y, z))\n        grid_index_array = index_voxel.T\n        pcd = o3d.t.geometry.PointCloud(device)\n        pcd.point.positions = o3d.core.Tensor(grid_index_array, dtype, device)\n\n        vals = np.repeat(np.expand_dims(img[x, y, z], -), , -)\n        pcd.point.colors = o3d.core.Tensor(vals, dtype, device)\n\n        dX, dY = dicom_slice.PixelSpacing\n        X = np.array((dicom_slice.ImageOrientationPatient[:]) + []) * dX\n        Y = np.array((dicom_slice.ImageOrientationPatient[:]) + []) * dY\n        S = np.array((dicom_slice.ImagePositionPatient) + [])\n\n        transform_matrix = np.array([X, Y, np.zeros((X)), S]).T\n        pcd.transform(o3d.core.Tensor(transform_matrix, dtype, device))\n        pcd_overall += pcd.to_legacy()\n\n     pcd_overall\n</code></pre>",
              "rawMarkdown": "When you have GPU turned on, all legacy API objects created by `open3d` are on the GPU:\n```python\nprint(type(o3d.geometry.PointCloud()), type(o3d.geometry.VoxelGrid()))\n```\nPrints: `<class 'open3d.cuda.pybind.geometry.PointCloud'> <class 'open3d.cuda.pybind.geometry.VoxelGrid'>`\n\nI tried the new `o3d.t.geometry.PointCloud()` (there is no equivalent for `VoxelGrid` in the new API):\n- Appending 2 `PointCloud` objects resulted in error for some unknown reason.\n- It did save 2 seconds: `7.05 s ± 119 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)`. Maybe it was because I used `float32`\nOnly `read_series_as_pcd` need to be modified from above:\n```python\ndef read_series_as_pcd(dir_path):\n    device = o3d.core.Device(\"CUDA:0\")\n    dtype = o3d.core.float32\n    pcd_overall = o3d.geometry.PointCloud()\n\n    for path in Path(dir_path).glob(\"*.dcm\"):\n        dicom_slice = dcmread(path)\n        img = np.expand_dims(dicom_slice.pixel_array, -1)\n        x, y, z = np.where(img)\n\n        index_voxel = np.vstack((x, y, z))\n        grid_index_array = index_voxel.T\n        pcd = o3d.t.geometry.PointCloud(device)\n        pcd.point.positions = o3d.core.Tensor(grid_index_array, dtype, device)\n\n        vals = np.repeat(np.expand_dims(img[x, y, z], -1), 3, -1)\n        pcd.point.colors = o3d.core.Tensor(vals, dtype, device)\n\n        dX, dY = dicom_slice.PixelSpacing\n        X = np.array(list(dicom_slice.ImageOrientationPatient[:3]) + [0]) * dX\n        Y = np.array(list(dicom_slice.ImageOrientationPatient[3:]) + [0]) * dY\n        S = np.array(list(dicom_slice.ImagePositionPatient) + [1])\n\n        transform_matrix = np.array([X, Y, np.zeros(len(X)), S]).T\n        pcd.transform(o3d.core.Tensor(transform_matrix, dtype, device))\n        pcd_overall += pcd.to_legacy()\n\n    return pcd_overall\n```",
              "votes": 1
            },
            {
              "id": 2950579,
              "postDate": "2024-08-07T17:18:35.200Z",
              "rawMarkdown": "",
              "isDeleted": true
            }
          ]
        }
      ]
    }
  ],
  "comments": [
    {
      "id": 2948844,
      "author_name": "coderRKJ",
      "author_url": "",
      "post_date": "2024-08-06T07:00:38.477000",
      "content": "<p>Your idea is great. I have been trying to do something like this by rotating 3D NumPy volumes with padding around it. It was not very successful.<br>\n<strong>2 questions on my end:</strong></p>\n<ol>\n<li>What happens when <code>dicom_slice.SliceThickness</code> is not an int.</li>\n<li>I tried visualizing the <code>grid</code> volume returned by DICOM series (study_id=46494080, series_id=1763376930) with <code>read_series_as_voxel_grid</code>, one of the slices looked like this:<br>\n<img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F1303569%2Fa43e29a490fd17c71c67d2a5edcbe99c%2FScreenshot%202024-08-06%20122715.png?generation=1722927491776227&amp;alt=media\" alt=\"\"></li>\n</ol>\n<p>I think the issue is in the last part: <code>grid[(coord[1], coord[0], coord[2])] = vals[index]</code>. May be these indices don't line up with <code>pcd_xyzd</code></p>",
      "votes": 1,
      "replies": [
        {
          "id": 2949274,
          "author_name": "Victor S",
          "author_url": "",
          "post_date": "2024-08-06T14:38:56.210000",
          "content": "<p>On the first question, my bad for leaving that bit in. Slice thickness is not relevant at all.</p>\n<p>On the second question, great catch! Also my bad for not checking. They indeed do not align with <code>pcd_xyzd</code>, just added the bugfix.<br>\n<img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F1734709%2F892ccd20d46ca8d6e1c67639610ae6d2%2F__results___7_0.png?generation=1722955580921247&amp;alt=media\" alt=\"\"></p>",
          "votes": 1,
          "replies": []
        }
      ]
    },
    {
      "id": 2949390,
      "author_name": "coderRKJ",
      "author_url": "",
      "post_date": "2024-08-06T16:04:39.863000",
      "content": "<p><a href=\"https://www.kaggle.com/vsahin\" target=\"_blank\">@vsahin</a> Great, I was also about to suggest not repeating <code>pixel_array</code> to reduce computational time along with using colors to store value. But you already did that. So here is a minor improvement:</p>\n<pre><code> ():\n    pcd_overall = o3d.geometry.PointCloud()\n\n     path  Path(dir_path).glob():\n        dicom_slice = dcmread(path)\n        img = np.expand_dims(dicom_slice.pixel_array, -)\n        x, y, z = np.where(img)\n\n        index_voxel = np.vstack((x, y, z))\n        grid_index_array = index_voxel.T\n        pcd = o3d.geometry.PointCloud(o3d.utility.Vector3dVector(grid_index_array))\n\n        vals = np.repeat(np.expand_dims(img[x, y, z], -), , -)\n        pcd.colors = o3d.utility.Vector3dVector(vals) \n\n        dX, dY = dicom_slice.PixelSpacing\n        X = np.array((dicom_slice.ImageOrientationPatient[:]) + []) * dX\n        Y = np.array((dicom_slice.ImageOrientationPatient[:]) + []) * dY\n        S = np.array((dicom_slice.ImagePositionPatient) + [])\n\n        transform_matrix = np.array([X, Y, np.zeros((X)), S]).T\n        pcd_overall += pcd.transform(transform_matrix)\n\n     pcd_overall\n\n ():\n    pcd_overall = read_series_as_pcd(dir_path)\n\n    path = (Path(dir_path).glob())\n    dicom_slice = dcmread(path)\n    dX, dY = dicom_slice.PixelSpacing\n\n    voxel_grid = o3d.geometry.VoxelGrid().create_from_point_cloud(pcd_overall, dX)\n\n    coords = np.array([voxel.grid_index  voxel  voxel_grid.get_voxels()])\n    vals = np.array([voxel.color[]  voxel  voxel_grid.get_voxels()])\n\n    size = np.(coords, axis=) + \n    grid = np.zeros((size[], size[], size[]))\n\n    grid[coords[:, ], coords[:, ], coords[:, ]] = vals\n\n     grid\n</code></pre>\n<p>This directly creates <code>pcd_overall</code> by summing PCDs: <code>pcd_overall += pcd.transform(transform_matrix)</code><br>\nAlso, removing loops in favor of NumPy indexing: <code>img[x, y, z]</code> and <code>grid[coords[:, 0], coords[:, 1], coords[:, 2]] = vals</code></p>\n<p>Your new code's <code>timeit</code> for <code>read_series_as_voxel_grid(train_images_path / \"46494080\" / \"1763376930\")</code>: </p>\n<pre><code>. s ±  ms per loop (mean ± std. dev. of  runs,  loop each)\n</code></pre>\n<p>My above modified code:</p>\n<pre><code>. s ±  ms per loop (mean ± std. dev. of  runs,  loop each)\n</code></pre>",
      "votes": 2,
      "replies": [
        {
          "id": 2949445,
          "author_name": "Victor S",
          "author_url": "",
          "post_date": "2024-08-06T16:36:05.820000",
          "content": "<p>Looks great! Next thing I am wondering is if we could get some speedup from CUDA for the point cloud transformation part <br>\n<a href=\"https://docs.cupy.dev/en/stable/reference/generated/cupyx.scipy.ndimage.affine_transform.html\" target=\"_blank\">https://docs.cupy.dev/en/stable/reference/generated/cupyx.scipy.ndimage.affine_transform.html</a></p>",
          "votes": 1,
          "replies": [
            {
              "id": 2950417,
              "author_name": "coderRKJ",
              "author_url": "",
              "post_date": "2024-08-07T14:48:07.043000",
              "content": "<p>When you have GPU turned on, all legacy API objects created by <code>open3d</code> are on the GPU:</p>\n<pre><code>((o3d.geometry.PointCloud()), (o3d.geometry.VoxelGrid()))\n</code></pre>\n<p>Prints: <code>&lt;class 'open3d.cuda.pybind.geometry.PointCloud'&gt; &lt;class 'open3d.cuda.pybind.geometry.VoxelGrid'&gt;</code></p>\n<p>I tried the new <code>o3d.t.geometry.PointCloud()</code> (there is no equivalent for <code>VoxelGrid</code> in the new API):</p>\n<ul>\n<li>Appending 2 <code>PointCloud</code> objects resulted in error for some unknown reason.</li>\n<li>It did save 2 seconds: <code>7.05 s ± 119 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)</code>. Maybe it was because I used <code>float32</code><br>\nOnly <code>read_series_as_pcd</code> need to be modified from above:</li>\n</ul>\n<pre><code> ():\n    device = o3d.core.Device()\n    dtype = o3d.core.float32\n    pcd_overall = o3d.geometry.PointCloud()\n\n     path  Path(dir_path).glob():\n        dicom_slice = dcmread(path)\n        img = np.expand_dims(dicom_slice.pixel_array, -)\n        x, y, z = np.where(img)\n\n        index_voxel = np.vstack((x, y, z))\n        grid_index_array = index_voxel.T\n        pcd = o3d.t.geometry.PointCloud(device)\n        pcd.point.positions = o3d.core.Tensor(grid_index_array, dtype, device)\n\n        vals = np.repeat(np.expand_dims(img[x, y, z], -), , -)\n        pcd.point.colors = o3d.core.Tensor(vals, dtype, device)\n\n        dX, dY = dicom_slice.PixelSpacing\n        X = np.array((dicom_slice.ImageOrientationPatient[:]) + []) * dX\n        Y = np.array((dicom_slice.ImageOrientationPatient[:]) + []) * dY\n        S = np.array((dicom_slice.ImagePositionPatient) + [])\n\n        transform_matrix = np.array([X, Y, np.zeros((X)), S]).T\n        pcd.transform(o3d.core.Tensor(transform_matrix, dtype, device))\n        pcd_overall += pcd.to_legacy()\n\n     pcd_overall\n</code></pre>",
              "votes": 1,
              "replies": []
            },
            {
              "id": 2950579,
              "author_name": "",
              "author_url": "",
              "post_date": "2024-08-07T17:18:35.200000",
              "content": "",
              "votes": 0,
              "replies": []
            }
          ]
        }
      ]
    }
  ],
  "raw_markdown_by_id": {
    "2948031": "Here is my attempt at loading series in 3D with the appropriate slice transforms: https://www.kaggle.com/code/vsahin/rsna-2024-dicom-3d-loading\n\nThe downside is, it is *extremely* slow and not really feasible for actually loading the dataset. So would really appreciate speedup tips.\n\n```\ndef read_series_as_pcd(dir_path):\n    pcds_xyz = []\n    pcds_d = []\n\n    paths = glob.glob(os.path.join(dir_path, \"*.dcm\"))\n    for path in paths:\n        dicom_slice = pydicom.read_file(path)\n        img = np.expand_dims(dicom_slice.pixel_array, -1)\n        pcd = o3d.geometry.PointCloud()\n        x, y, z = np.where(img)\n\n        index_voxel = np.vstack((x, y, z))\n        grid_index_array = index_voxel.T\n        pcd.points = o3d.utility.Vector3dVector(grid_index_array)\n\n        vals = np.array([img[x, y, z] for x, y, z in grid_index_array])\n\n        dX, dY = dicom_slice.PixelSpacing\n        X = np.array(list(dicom_slice.ImageOrientationPatient[:3]) + [0]) * dX\n        Y = np.array(list(dicom_slice.ImageOrientationPatient[3:]) + [0]) * dY\n        S = np.array(list(dicom_slice.ImagePositionPatient) + [1])\n\n        transform_matrix = np.array([X, Y, np.zeros(len(X)), S]).T\n        transformed_pcd = pcd.transform(transform_matrix)\n\n        pcds_xyz.extend(transformed_pcd.points)\n        pcds_d.extend(vals)\n    \n    pcd_xyzd = np.hstack((pcds_xyz, np.expand_dims(pcds_d, -1)))\n    return pcd_xyzd\n\n```\n\n```\ndef read_series_as_voxel_grid(dir_path):\n    pcd_xyzd = read_series_as_pcd(dir_path)\n\n    pcd_overall = o3d.geometry.PointCloud()\n    pcd_overall.points = o3d.utility.Vector3dVector(pcd_xyzd[:, :3])\n    pcd_overall.colors = o3d.utility.Vector3dVector(np.repeat(np.expand_dims(pcd_xyzd[:, 3], -1), 3, -1).astype(np.int32))\n\n    paths = glob.glob(os.path.join(dir_path, \"*.dcm\"))\n    dicom_slice = pydicom.read_file(paths[0])\n    dX, dY = dicom_slice.PixelSpacing\n\n    voxel_grid = o3d.geometry.VoxelGrid().create_from_point_cloud(pcd_overall, dX)\n\n    coords = np.array([voxel.grid_index for voxel in voxel_grid.get_voxels()])\n    vals = np.array([voxel.color[0] for voxel in voxel_grid.get_voxels()])\n\n    pcd_overall.clear()\n    voxel_grid.clear()\n\n    size = np.max(coords, axis=0) + 1\n    grid = np.zeros((size[0], size[1], size[2]))\n\n    for index, coord in enumerate(coords):\n        grid[(coord[0], coord[1], coord[2])] = vals[index]\n\n    f = pgzip.PgzipFile(cache_path, \"w\")\n    np.save(f, grid)\n    f.close()\n\n    del pcd_overall\n    del voxel_grid\n\n    return grid\n```",
    "2948844": "Your idea is great. I have been trying to do something like this by rotating 3D NumPy volumes with padding around it. It was not very successful.\n**2 questions on my end:**\n1. What happens when `dicom_slice.SliceThickness` is not an int.\n1. I tried visualizing the `grid` volume returned by DICOM series (study_id=46494080, series_id=1763376930) with `read_series_as_voxel_grid`, one of the slices looked like this:\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F1303569%2Fa43e29a490fd17c71c67d2a5edcbe99c%2FScreenshot%202024-08-06%20122715.png?generation=1722927491776227&alt=media)\n\nI think the issue is in the last part: `grid[(coord[1], coord[0], coord[2])] = vals[index]`. May be these indices don't line up with `pcd_xyzd`",
    "2949390": "@vsahin Great, I was also about to suggest not repeating `pixel_array` to reduce computational time along with using colors to store value. But you already did that. So here is a minor improvement:\n```python\ndef read_series_as_pcd(dir_path):\n    pcd_overall = o3d.geometry.PointCloud()\n\n    for path in Path(dir_path).glob(\"*.dcm\"):\n        dicom_slice = dcmread(path)\n        img = np.expand_dims(dicom_slice.pixel_array, -1)\n        x, y, z = np.where(img)\n\n        index_voxel = np.vstack((x, y, z))\n        grid_index_array = index_voxel.T\n        pcd = o3d.geometry.PointCloud(o3d.utility.Vector3dVector(grid_index_array))\n\n        vals = np.repeat(np.expand_dims(img[x, y, z], -1), 3, -1)\n        pcd.colors = o3d.utility.Vector3dVector(vals) # Converted to np.float64\n\n        dX, dY = dicom_slice.PixelSpacing\n        X = np.array(list(dicom_slice.ImageOrientationPatient[:3]) + [0]) * dX\n        Y = np.array(list(dicom_slice.ImageOrientationPatient[3:]) + [0]) * dY\n        S = np.array(list(dicom_slice.ImagePositionPatient) + [1])\n\n        transform_matrix = np.array([X, Y, np.zeros(len(X)), S]).T\n        pcd_overall += pcd.transform(transform_matrix)\n\n    return pcd_overall\n\ndef read_series_as_voxel_grid(dir_path):\n    pcd_overall = read_series_as_pcd(dir_path)\n\n    path = next(Path(dir_path).glob(\"*.dcm\"))\n    dicom_slice = dcmread(path)\n    dX, dY = dicom_slice.PixelSpacing\n\n    voxel_grid = o3d.geometry.VoxelGrid().create_from_point_cloud(pcd_overall, dX)\n\n    coords = np.array([voxel.grid_index for voxel in voxel_grid.get_voxels()])\n    vals = np.array([voxel.color[0] for voxel in voxel_grid.get_voxels()])\n\n    size = np.max(coords, axis=0) + 1\n    grid = np.zeros((size[0], size[1], size[2]))\n\n    grid[coords[:, 0], coords[:, 1], coords[:, 2]] = vals\n\n    return grid\n```\nThis directly creates `pcd_overall` by summing PCDs: `pcd_overall += pcd.transform(transform_matrix)`\nAlso, removing loops in favor of NumPy indexing: `img[x, y, z]` and `grid[coords[:, 0], coords[:, 1], coords[:, 2]] = vals`\n\nYour new code's `timeit` for `read_series_as_voxel_grid(train_images_path / \"46494080\" / \"1763376930\")`: \n```\n15.4 s ± 363 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n```\nMy above modified code:\n```\n9.05 s ± 173 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n```"
  }
}