{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"**See the MRI: from DICOMt to Slices & Studies**\n\nIn the previous notebook ([this one](https://www.kaggle.com/code/h17ann/2-rsna-knee-dicom-metadata-explorer)), we only looked at the DICOM headers.\n\nThis time we load the pixel data and see how a knee MRI is built from individual DICOM files.\n\nWe will:\n\n- open one DICOM image\n- look at the pixel array\n- display a slice\n- load a complete MRI series\n- sort the slices\n- build a 3D stack\n- show several slices from the same series\n- compare different series from the same study","metadata":{}},{"cell_type":"markdown","source":"## 1. Imports","metadata":{}},{"cell_type":"code","source":"import kagglehub\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport pydicom","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-25T19:08:21.662713Z","iopub.execute_input":"2026-08-25T19:08:21.663096Z","iopub.status.idle":"2026-08-25T19:08:21.675115Z","shell.execute_reply.started":"2026-08-25T19:08:21.663057Z","shell.execute_reply":"2026-08-25T19:08:21.674065Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Load the series table","metadata":{}},{"cell_type":"code","source":"DATA_DIR = Path(\"/kaggle/input/competitions/rsna-knee-abnormality-detection\")\n# Download the data if it is not already attached\nif not DATA_DIR.exists():\n    DATA_DIR = Path(\n        kagglehub.competition_download(\n            \"rsna-knee-abnormality-detection\"\n        )\n    )\nTRAIN_DIR = DATA_DIR / \"train_series\"\n\ntrain_series = pd.read_csv(DATA_DIR / \"train_series.csv\")\n\ndisplay(train_series.head())\nprint(\"Studies:\", train_series[\"StudyInstanceUID\"].nunique())\nprint(\"Series :\", train_series[\"SeriesInstanceUID\"].nunique())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-25T19:08:21.676381Z","iopub.execute_input":"2026-08-25T19:08:21.676721Z","iopub.status.idle":"2026-08-25T19:08:21.781859Z","shell.execute_reply.started":"2026-08-25T19:08:21.676672Z","shell.execute_reply":"2026-08-25T19:08:21.78093Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The image files are stored as:\n\n`StudyInstanceUID / SeriesInstanceUID / *.dcm`","metadata":{}},{"cell_type":"markdown","source":"## 3. Pick one MRI series","metadata":{}},{"cell_type":"code","source":"row = train_series.iloc[0]\n\nstudy_uid = row[\"StudyInstanceUID\"]\nseries_uid = row[\"SeriesInstanceUID\"]\n\nseries_dir = TRAIN_DIR / str(study_uid) / str(series_uid)\ndicom_files = sorted(series_dir.glob(\"*.dcm\"))\n\nprint(\"Study :\", study_uid)\nprint(\"Series:\", series_uid)\nprint(\"Slices:\", len(dicom_files))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-25T19:08:21.782911Z","iopub.execute_input":"2026-08-25T19:08:21.783207Z","iopub.status.idle":"2026-08-25T19:08:21.790655Z","shell.execute_reply.started":"2026-08-25T19:08:21.783182Z","shell.execute_reply":"2026-08-25T19:08:21.789693Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Open one DICOM image","metadata":{}},{"cell_type":"code","source":"ds = pydicom.dcmread(dicom_files[0])\n\nprint(\"Rows   :\", ds.Rows)\nprint(\"Columns:\", ds.Columns)\nprint(\"dtype  :\", ds.pixel_array.dtype)\nprint(\"shape  :\", ds.pixel_array.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-25T19:08:21.791778Z","iopub.execute_input":"2026-08-25T19:08:21.79207Z","iopub.status.idle":"2026-08-25T19:08:21.811908Z","shell.execute_reply.started":"2026-08-25T19:08:21.792037Z","shell.execute_reply":"2026-08-25T19:08:21.81031Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"`pixel_array` gives us the image as a NumPy array.\n\nFor a standard single-slice DICOM file, the result is a 2D matrix.","metadata":{}},{"cell_type":"markdown","source":"## 5. Display one slice","metadata":{}},{"cell_type":"code","source":"image = ds.pixel_array\n\nplt.figure(figsize=(6, 6))\nplt.imshow(image, cmap=\"gray\")\nplt.axis(\"off\")\nplt.title(\"One DICOM slice\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-25T19:08:21.813209Z","iopub.execute_input":"2026-08-25T19:08:21.813509Z","iopub.status.idle":"2026-08-25T19:08:22.022885Z","shell.execute_reply.started":"2026-08-25T19:08:21.813457Z","shell.execute_reply":"2026-08-25T19:08:22.021983Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. A small intensity check","metadata":{}},{"cell_type":"code","source":"print(\"Minimum:\", image.min())\nprint(\"Maximum:\", image.max())\nprint(\"Mean   :\", image.mean())\nprint(\"Median :\", np.median(image))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-25T19:08:22.02413Z","iopub.execute_input":"2026-08-25T19:08:22.024524Z","iopub.status.idle":"2026-08-25T19:08:22.033623Z","shell.execute_reply.started":"2026-08-25T19:08:22.024462Z","shell.execute_reply":"2026-08-25T19:08:22.032655Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The stored pixel values are not automatically normalized to 0-1.\n\nFor a quick visualization, imshow automatically scales the displayed intensity range.","metadata":{}},{"cell_type":"markdown","source":"## 7. Sort all slices in one series","metadata":{}},{"cell_type":"code","source":"def slice_number(path):\n    ds = pydicom.dcmread(path, stop_before_pixels=True)\n\n    if \"InstanceNumber\" in ds:\n        return int(ds.InstanceNumber)\n\n    return 0\n\n\ndicom_files = sorted(dicom_files, key=slice_number)\n\nprint(\"Sorted slices:\", len(dicom_files))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-25T19:08:22.034968Z","iopub.execute_input":"2026-08-25T19:08:22.035992Z","iopub.status.idle":"2026-08-25T19:08:22.089876Z","shell.execute_reply.started":"2026-08-25T19:08:22.035945Z","shell.execute_reply":"2026-08-25T19:08:22.088833Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"`InstanceNumber` is a simple way to order the slices when it is present.\n\nLater, for more careful 3D geometry work, DICOM position and orientation tags are also important.","metadata":{}},{"cell_type":"markdown","source":"## 8. Load the complete series","metadata":{}},{"cell_type":"code","source":"volume = []\n\nfor path in dicom_files:\n    ds = pydicom.dcmread(path)\n    volume.append(ds.pixel_array)\n\nvolume = np.stack(volume)\n\nprint(\"Volume shape:\", volume.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-25T19:08:22.091178Z","iopub.execute_input":"2026-08-25T19:08:22.091557Z","iopub.status.idle":"2026-08-25T19:08:22.161068Z","shell.execute_reply.started":"2026-08-25T19:08:22.091521Z","shell.execute_reply":"2026-08-25T19:08:22.159775Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The shape is:\n\n`(number of slices, rows, columns)`","metadata":{}},{"cell_type":"markdown","source":"## 9. Show slices across the series","metadata":{}},{"cell_type":"code","source":"n = 6\nindices = np.linspace(0, len(volume) - 1, n, dtype=int)\n\nfig, axes = plt.subplots(1, n, figsize=(16, 4))\n\nfor ax, idx in zip(axes, indices):\n    ax.imshow(volume[idx], cmap=\"gray\")\n    ax.set_title(f\"Slice {idx}\")\n    ax.axis(\"off\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-25T19:08:22.162283Z","iopub.execute_input":"2026-08-25T19:08:22.162642Z","iopub.status.idle":"2026-08-25T19:08:22.863553Z","shell.execute_reply.started":"2026-08-25T19:08:22.162601Z","shell.execute_reply":"2026-08-25T19:08:22.862618Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Moving through the slices lets us follow the anatomy through the knee instead of looking at only one 2D image.","metadata":{}},{"cell_type":"markdown","source":"## 10. Look at the middle of the series","metadata":{}},{"cell_type":"code","source":"middle = len(volume) // 2\n\nplt.figure(figsize=(6, 6))\nplt.imshow(volume[middle], cmap=\"gray\")\nplt.axis(\"off\")\nplt.title(f\"Middle slice ({middle})\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-25T19:08:22.864711Z","iopub.execute_input":"2026-08-25T19:08:22.864999Z","iopub.status.idle":"2026-08-25T19:08:23.070015Z","shell.execute_reply.started":"2026-08-25T19:08:22.864974Z","shell.execute_reply":"2026-08-25T19:08:23.068773Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 11. How many slices are there per series?","metadata":{}},{"cell_type":"code","source":"slice_counts = []\n\nfor row in train_series.head(300).itertuples(index=False):\n    folder = (\n        TRAIN_DIR\n        / str(row.StudyInstanceUID)\n        / str(row.SeriesInstanceUID)\n    )\n\n    slice_counts.append(len(list(folder.glob(\"*.dcm\"))))\n\nslice_counts = pd.Series(slice_counts, name=\"slices_per_series\")\n\ndisplay(slice_counts.describe())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-25T19:08:23.071677Z","iopub.execute_input":"2026-08-25T19:08:23.072167Z","iopub.status.idle":"2026-08-25T19:08:23.391431Z","shell.execute_reply.started":"2026-08-25T19:08:23.072132Z","shell.execute_reply":"2026-08-25T19:08:23.390636Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(10, 5))\nplt.hist(slice_counts, bins=25)\nplt.xlabel(\"Slices per series\")\nplt.ylabel(\"Number of series\")\nplt.title(\"Number of DICOM slices per MRI series\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-25T19:08:23.393226Z","iopub.execute_input":"2026-08-25T19:08:23.393598Z","iopub.status.idle":"2026-08-25T19:08:23.590754Z","shell.execute_reply.started":"2026-08-25T19:08:23.393568Z","shell.execute_reply":"2026-08-25T19:08:23.589788Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"NB: This quick check uses only the first 300 series so the notebook stays light.","metadata":{}},{"cell_type":"markdown","source":"## 12. Different series from the same study","metadata":{}},{"cell_type":"code","source":"series_per_study = (\n    train_series\n    .groupby(\"StudyInstanceUID\")\n    .size()\n    .sort_values(ascending=False)\n)\n\nexample_study = series_per_study.index[0]\n\nstudy_series = train_series[\n    train_series[\"StudyInstanceUID\"] == example_study\n].copy()\n\ndisplay(study_series)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-25T19:08:23.594559Z","iopub.execute_input":"2026-08-25T19:08:23.594973Z","iopub.status.idle":"2026-08-25T19:08:23.617442Z","shell.execute_reply.started":"2026-08-25T19:08:23.594943Z","shell.execute_reply":"2026-08-25T19:08:23.616461Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"One MRI study can contain several acquisitions.\n\nThe next cell shows the middle slice of a few series from the same study.","metadata":{}},{"cell_type":"code","source":"n_show = min(6, len(study_series))\n\nfig, axes = plt.subplots(1, n_show, figsize=(18, 4))\n\nif n_show == 1:\n    axes = [axes]\n\nfor ax, row in zip(axes, study_series.head(n_show).itertuples(index=False)):\n\n    folder = (\n        TRAIN_DIR\n        / str(row.StudyInstanceUID)\n        / str(row.SeriesInstanceUID)\n    )\n\n    files = list(folder.glob(\"*.dcm\"))\n    files = sorted(files, key=slice_number)\n\n    middle_file = files[len(files) // 2]\n    ds = pydicom.dcmread(middle_file)\n\n    ax.imshow(ds.pixel_array, cmap=\"gray\")\n    ax.axis(\"off\")\n    ax.set_title(\n        f\"{row.Anatomical_Plane}\\n\"\n        f\"Fluid: {row.Fluid_Sensitive}\"\n    )\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-25T19:08:23.618706Z","iopub.execute_input":"2026-08-25T19:08:23.619024Z","iopub.status.idle":"2026-08-25T19:08:24.887783Z","shell.execute_reply.started":"2026-08-25T19:08:23.618988Z","shell.execute_reply":"2026-08-25T19:08:24.886796Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Even inside the same examination, the appearance changes from one series to another because the acquisition plane and sequence characteristics are different.","metadata":{}},{"cell_type":"markdown","source":"## 13. Quick look at spatial resolution","metadata":{}},{"cell_type":"code","source":"ds = pydicom.dcmread(dicom_files[len(dicom_files) // 2], stop_before_pixels=True)\n\nprint(\"Pixel spacing          :\", ds.get(\"PixelSpacing\"))\nprint(\"Slice thickness        :\", ds.get(\"SliceThickness\"))\nprint(\"Spacing between slices :\", ds.get(\"SpacingBetweenSlices\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-25T19:08:24.888989Z","iopub.execute_input":"2026-08-25T19:08:24.889302Z","iopub.status.idle":"2026-08-25T19:08:24.897417Z","shell.execute_reply.started":"2026-08-25T19:08:24.889266Z","shell.execute_reply":"2026-08-25T19:08:24.896404Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"`PixelSpacing` describes the physical size of a pixel inside the 2D image.\n\n`SliceThickness` and `SpacingBetweenSlices` describe the through-plane spatial information.\n\nSo the NumPy shape tells us the number of pixels, while the DICOM metadata tells us their physical size.","metadata":{}},{"cell_type":"markdown","source":"## Takeaways","metadata":{}},{"cell_type":"markdown","source":"- A DICOM MRI series is made of multiple slices.\n- `pixel_array` gives access to the actual image values.\n- A complete series can be stacked into a 3D NumPy array.\n- One study usually contains several MRI series.\n- Different series can show the same knee in different planes and with different contrast.\n- Image dimensions and physical resolution are two different things.\n\n### Next notebook\n\n[**4. RSNA Knee -> Know the Targets: 12 Abnormalities Explained**](https://www.kaggle.com/code/h17ann/4-rsna-knee-know-the-12-targets)\n\nNext, we will step away from the DICOM format and look at what the 12 prediction targets actually mean... stay tuned !","metadata":{}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}