{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Dicom to Coronals with Masks\n\nThe coronal views take up less space but may not have enough data. You can adjust the interval to change number of coronal images.\n\nThis notebook is for one series","metadata":{}},{"cell_type":"markdown","source":"1. Sorted list of images in a series\n2. Boiler plate dicom to array\n3. Create 3d stack\n4. Extract coronal views\n5. Refine coronals to remove mostly empty images\n6. Put it all together into a function and add coronal masks\n7. Run function to get 200+ filtered coronals\n8. Combine 3 images at a time into png files and create ~8 'frames' per patient","metadata":{}},{"cell_type":"code","source":"!pip install dicomsdl -q","metadata":{"execution":{"iopub.status.busy":"2023-10-27T15:52:41.711823Z","iopub.execute_input":"2023-10-27T15:52:41.712915Z","iopub.status.idle":"2023-10-27T15:52:54.606771Z","shell.execute_reply.started":"2023-10-27T15:52:41.71287Z","shell.execute_reply":"2023-10-27T15:52:54.605518Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import numpy as np\nfrom matplotlib import pyplot as plt\nimport pydicom\nimport dicomsdl\nimport os\nimport nibabel as nib\nimport pandas as pd\nfrom PIL import Image\nimport cv2\nfrom glob import glob\nfrom pathlib import Path\nfrom sklearn.model_selection import StratifiedGroupKFold","metadata":{"execution":{"iopub.status.busy":"2023-10-27T15:52:54.609144Z","iopub.execute_input":"2023-10-27T15:52:54.609475Z","iopub.status.idle":"2023-10-27T15:52:55.392546Z","shell.execute_reply.started":"2023-10-27T15:52:54.609445Z","shell.execute_reply":"2023-10-27T15:52:55.391597Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# convenience directory variables\nroot_dir = '/kaggle/input/rsna-2023-abdominal-trauma-detection'\ntrain_dir = f'{root_dir}/train_images'\nseg_dir = f'{root_dir}/segmentations'\n\n# study and series id dataframe\ndf = pd.read_csv(f'{root_dir}/train_series_meta.csv')","metadata":{"execution":{"iopub.status.busy":"2023-10-27T15:52:55.39368Z","iopub.execute_input":"2023-10-27T15:52:55.394214Z","iopub.status.idle":"2023-10-27T15:52:55.422168Z","shell.execute_reply.started":"2023-10-27T15:52:55.394167Z","shell.execute_reply":"2023-10-27T15:52:55.421363Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Sample case","metadata":{}},{"cell_type":"code","source":"# Take one sample series with segmentation\nsegmention_sample = 10000\nsegmented_case_ids = df[df.series_id==segmention_sample].values[0][:2].astype(int)\n\nimg_dir = f'{train_dir}/{segmented_case_ids[0]}/{segmented_case_ids[1]}'\n","metadata":{"execution":{"iopub.status.busy":"2023-10-27T15:52:55.423673Z","iopub.execute_input":"2023-10-27T15:52:55.424357Z","iopub.status.idle":"2023-10-27T15:52:55.438442Z","shell.execute_reply.started":"2023-10-27T15:52:55.424316Z","shell.execute_reply":"2023-10-27T15:52:55.437561Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Show mask","metadata":{}},{"cell_type":"code","source":"#mask\nseg_fn = f'{seg_dir}/{segmention_sample}.nii'\nmask = nib.load(seg_fn).get_fdata()\n\nim = mask.astype(np.uint8)\nim = np.swapaxes(im,1,0)\nim = np.rot90(im, 1, (1,2))\nim = im[::-1,:,:]\nplt.imshow(im[240,:, :])","metadata":{"execution":{"iopub.status.busy":"2023-10-27T15:52:55.442083Z","iopub.execute_input":"2023-10-27T15:52:55.442812Z","iopub.status.idle":"2023-10-27T15:53:00.186077Z","shell.execute_reply.started":"2023-10-27T15:52:55.442771Z","shell.execute_reply":"2023-10-27T15:53:00.185004Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Mask organ label mapping\nlabel_dict = {1: 'liver',\n2:'spleen',\n3:'kidney_left',\n4:'kidney_right',\n5 :'bowel'}","metadata":{"execution":{"iopub.status.busy":"2023-10-27T15:53:00.187496Z","iopub.execute_input":"2023-10-27T15:53:00.187829Z","iopub.status.idle":"2023-10-27T15:53:00.192638Z","shell.execute_reply.started":"2023-10-27T15:53:00.187801Z","shell.execute_reply":"2023-10-27T15:53:00.191673Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Sorted list of images in series","metadata":{}},{"cell_type":"code","source":"#one series from one exam\n\n#get filenames\nim_list = []\nfor dirname, _, filenames in os.walk(img_dir):\n    for filename in filenames:\n        im_list.append(os.path.join(dirname, filename))\n\n#sort\nim_list.sort(key = lambda o: int(o.split('/')[-1][:-4]))\nlen(im_list)","metadata":{"execution":{"iopub.status.busy":"2023-10-27T15:53:00.193808Z","iopub.execute_input":"2023-10-27T15:53:00.194114Z","iopub.status.idle":"2023-10-27T15:53:00.413655Z","shell.execute_reply.started":"2023-10-27T15:53:00.194086Z","shell.execute_reply":"2023-10-27T15:53:00.41235Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Convert Dicom functions","metadata":{}},{"cell_type":"code","source":"#Read dicom with dicomsdl\ndef dcmread_to_array(fn):\n    dcm = dicomsdl.open(fn)\n\n\n    #dcm = pydicom.dcmread(fn)\n    return dcm_to_array(dcm) \n\ndef standardize_pixel_array(dcm: pydicom.dataset.FileDataset) -> np.ndarray:\n    \"\"\"\n    Source : https://www.kaggle.com/competitions/rsna-2023-abdominal-trauma-detection/discussion/427217\n    \"\"\"\n    # Correct DICOM pixel_array if PixelRepresentation == 1.\n    pixel_array = dcm.pixelData() #dcm.pixel_array\n    if dcm.PixelRepresentation == 1 and dcm.HighBit == 12:\n        bit_shift = dcm.BitsAllocated - dcm.BitsStored\n        dtype = pixel_array.dtype \n        new_array = (pixel_array << bit_shift).astype(dtype) >>  bit_shift\n        pixel_array = pydicom.pixel_data_handlers.util.apply_modality_lut(new_array, dcm)\n    return pixel_array\n\n\n# Made some changes after testing with the four HighBit/PixelRepresentation combinations\n# HighBit  PixelRepresentation\n# 15       1                      \n# 11       0                      \n# 12       1                       \n# 15       0             \ndef dcm_to_array(dcm, width = None, level = None, scaled = True, standardize = True):\n    if standardize == True:\n        arr = standardize_pixel_array(dcm)\n        arr = arr.astype('float32')\n    else:\n        arr = dcm.pixel_array.astype('float32')\n    \n    #slope, intercept\n    slope = 1\n    intercept = 0\n    if \"RescaleIntercept\" in dcm and \"RescaleSlope\" in dcm:\n        intercept = float(dcm.RescaleIntercept)\n        slope = float(dcm.RescaleSlope)\n    arr = (arr * slope) + intercept\n\n    #window\n    if width is None or level is None:\n        width,level = get_window_from_dicom(dcm)\n    \n    \n    upper, lower = level+width//2, level-width//2\n    arr = np.clip(arr, lower, upper)\n    arr = (arr - lower) / (upper - lower)\n        \n    if dcm.PhotometricInterpretation == \"MONOCHROME1\":\n        arr = 1 - arr\n  \n    if scaled:\n        arr = (arr * 255).astype(np.uint8) \n    return arr\n\ndef get_window_from_dicom(dcm, default = (400, 50)):\n    \"\"\"\n    Returns window width and window center values or first example if MultiValue\n    Strips comma from value if present (seen in a different dataset)\n    If no window width/level is provided or available, returns default.\n    \"\"\"\n    width, level = default\n\n    if \"WindowWidth\" in dcm:\n        width = dcm.WindowWidth\n        if isinstance(width, pydicom.multival.MultiValue):\n            width = float(width[0])\n        else:\n            width = float(str(width).replace(',', ''))\n\n    if \"WindowCenter\" in dcm:\n        level = dcm.WindowCenter\n        if isinstance(level, pydicom.multival.MultiValue):\n            level = float(level[0])\n        else:\n            level = float(str(level).replace(',', ''))\n            \n    return width, level","metadata":{"execution":{"iopub.status.busy":"2023-10-27T15:53:00.41563Z","iopub.execute_input":"2023-10-27T15:53:00.415956Z","iopub.status.idle":"2023-10-27T15:53:00.429787Z","shell.execute_reply.started":"2023-10-27T15:53:00.415927Z","shell.execute_reply":"2023-10-27T15:53:00.428528Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Read Dicom and convert to stack","metadata":{}},{"cell_type":"code","source":"stack= np.array([dcmread_to_array(fn) for fn in im_list])                      \nstack.shape","metadata":{"execution":{"iopub.status.busy":"2023-10-27T15:53:00.431378Z","iopub.execute_input":"2023-10-27T15:53:00.43169Z","iopub.status.idle":"2023-10-27T15:53:07.897271Z","shell.execute_reply.started":"2023-10-27T15:53:00.431664Z","shell.execute_reply":"2023-10-27T15:53:07.896182Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Coronal view (ignore pixels at front and back and take every nth image)","metadata":{}},{"cell_type":"code","source":"ignore_pixels = 25\n\n#Change this to get more images\ninterval = 10\n\ncor = stack[:,ignore_pixels:-ignore_pixels:interval,:]\ncor = np.swapaxes(cor,0,1)\ncor.shape","metadata":{"execution":{"iopub.status.busy":"2023-10-27T15:53:07.898447Z","iopub.execute_input":"2023-10-27T15:53:07.898759Z","iopub.status.idle":"2023-10-27T15:53:07.90612Z","shell.execute_reply.started":"2023-10-27T15:53:07.89873Z","shell.execute_reply":"2023-10-27T15:53:07.905058Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Show one image","metadata":{}},{"cell_type":"code","source":"im = cor[20]\nplt.imshow(im, cmap = 'gray')","metadata":{"execution":{"iopub.status.busy":"2023-10-27T15:53:07.907328Z","iopub.execute_input":"2023-10-27T15:53:07.907718Z","iopub.status.idle":"2023-10-27T15:53:08.238042Z","shell.execute_reply.started":"2023-10-27T15:53:07.907682Z","shell.execute_reply":"2023-10-27T15:53:08.236996Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Show images (and masks) function","metadata":{}},{"cell_type":"code","source":"#Show images ( and masks if available)\n#https://stackoverflow.com/questions/41071947/how-to-remove-the-space-between-subplots-in-matplotlib-pyplot\nfrom matplotlib import gridspec\n\ndef show_images(ims, masks=None, cols = 6):\n    rows = int(np.ceil(len(ims)/cols))\n    img_count = 0\n    \n    fig = plt.figure(figsize=(cols+1, rows+1)) \n\n    gs = gridspec.GridSpec(rows, cols,\n         wspace=0.0, hspace=0.0, \n         top=1.-0.5/(rows+1), bottom=0.5/(rows+1), \n         left=0.5/(cols+1), right=1-0.5/(cols+1)) \n    \n    for i in range(rows):\n        for j in range(cols):\n            if img_count < len(ims):\n                ax= plt.subplot(gs[i,j])\n                ax.imshow(ims[img_count], cmap='gray')\n                if masks is not None:\n                    ax.imshow(masks[img_count], alpha=0.5)\n                ax.set_xticklabels([])\n                ax.set_yticklabels([])\n                \n                img_count+=1\n        \n    plt.show()","metadata":{"execution":{"iopub.status.busy":"2023-10-27T15:53:08.239393Z","iopub.execute_input":"2023-10-27T15:53:08.240081Z","iopub.status.idle":"2023-10-27T15:53:08.249842Z","shell.execute_reply.started":"2023-10-27T15:53:08.240042Z","shell.execute_reply":"2023-10-27T15:53:08.248804Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Show  initial (unfiltered) coronal images","metadata":{}},{"cell_type":"code","source":"#show initial coronals\nshow_images(cor, cols = 8)","metadata":{"execution":{"iopub.status.busy":"2023-10-27T15:53:08.251323Z","iopub.execute_input":"2023-10-27T15:53:08.251693Z","iopub.status.idle":"2023-10-27T15:53:12.710661Z","shell.execute_reply.started":"2023-10-27T15:53:08.251659Z","shell.execute_reply":"2023-10-27T15:53:12.709492Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Filter by non zero value counts","metadata":{}},{"cell_type":"code","source":"#Number of non zero values as a percentage of max\nvals = np.array([np.count_nonzero(cor[i].flatten()) for i in range(len(cor))])\nmost = vals.max()\nvals = np.array([round(val/most * 100) for val in vals])\nvals","metadata":{"execution":{"iopub.status.busy":"2023-10-27T15:53:12.71435Z","iopub.execute_input":"2023-10-27T15:53:12.71468Z","iopub.status.idle":"2023-10-27T15:53:12.727151Z","shell.execute_reply.started":"2023-10-27T15:53:12.714646Z","shell.execute_reply":"2023-10-27T15:53:12.726246Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Get index for values over threshold\nnonzero_threshold = 45\nlen(cor[np.where(vals > nonzero_threshold)])","metadata":{"execution":{"iopub.status.busy":"2023-10-27T15:53:12.728712Z","iopub.execute_input":"2023-10-27T15:53:12.729002Z","iopub.status.idle":"2023-10-27T15:53:12.747014Z","shell.execute_reply.started":"2023-10-27T15:53:12.728978Z","shell.execute_reply":"2023-10-27T15:53:12.74575Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Only use indices with enough nonzero values \n\nNote: You can adjust threshold value higher to tighten this up even more","metadata":{}},{"cell_type":"code","source":"#plot better coronal slices\nbetter_cor = cor[np.where(vals > nonzero_threshold)]\nshow_images(better_cor)","metadata":{"execution":{"iopub.status.busy":"2023-10-27T15:53:12.748375Z","iopub.execute_input":"2023-10-27T15:53:12.748682Z","iopub.status.idle":"2023-10-27T15:53:14.595159Z","shell.execute_reply.started":"2023-10-27T15:53:12.748656Z","shell.execute_reply":"2023-10-27T15:53:14.594058Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Put it all together in a function","metadata":{}},{"cell_type":"code","source":"def create_series_path(df, image_dir, series_id):\n    study_id = df[df.series_id==series_id].values[0][0].astype(int)\n    return f'{image_dir}/{study_id}/{series_id}'\n\ndef create_series_path_from_patientid(df, image_dir, patient_id):\n    series_id = df[df.patient_id==patient_id]['series_id'].values[0]\n    return f'{image_dir}/{patient_id}/{series_id}'\n\ndef mask_coronal(mask):\n    im = mask.astype(np.uint8)\n    im = np.swapaxes(im,1,0)\n    im = np.rot90(im, 1, (1,2))\n    im = im[::-1,:,:]\n    return im\n\ndef dicom_to_coronals(series_path, \n                      interval = 10, \n                      ignore_pixels = 25, \n                      nonzero_threshold = 45,\n                     get_mask=True):\n    #get filenames\n    im_list = []\n    for dirname, _, filenames in os.walk(series_path):\n        for filename in filenames:\n            im_list.append(os.path.join(dirname, filename))\n\n    #sort\n    im_list.sort(key = lambda o: int(o.split('/')[-1][:-4]))\n    \n    #stack\n    stack= np.array([dcmread_to_array(fn) for fn in im_list])\n\n    #unfiltered coronals\n    cor = stack[:,ignore_pixels:-ignore_pixels:interval,:]\n    cor = np.swapaxes(cor,0,1)\n    \n    #nonzero values\n    vals = np.array([np.count_nonzero(cor[i].flatten()) for i in range(len(cor))])\n    most = vals.max()\n    vals = np.array([round(val/most * 100) for val in vals])\n    idx = np.where(vals > nonzero_threshold)\n    \n    #mask\n    series_id = series_path.split('/')[-1]\n    mask_fn = f'{seg_dir}/{series_id}.nii'\n    if os.path.exists(mask_fn):\n        mask = nib.load(mask_fn).get_fdata()\n        mask = mask_coronal(mask)\n        mask = mask[ignore_pixels:-ignore_pixels:interval,:, :]\n        mask = mask[idx]\n    else:\n        mask = None\n    \n    #return filtered coronals and mask if available\n    return cor[idx], mask","metadata":{"execution":{"iopub.status.busy":"2023-10-27T16:01:08.671929Z","iopub.execute_input":"2023-10-27T16:01:08.672367Z","iopub.status.idle":"2023-10-27T16:01:08.685352Z","shell.execute_reply.started":"2023-10-27T16:01:08.672333Z","shell.execute_reply":"2023-10-27T16:01:08.684269Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Add segmentation","metadata":{}},{"cell_type":"code","source":"cor,mask = dicom_to_coronals(img_dir)\ncor.shape, mask.shape","metadata":{"execution":{"iopub.status.busy":"2023-10-27T15:58:01.061528Z","iopub.execute_input":"2023-10-27T15:58:01.061941Z","iopub.status.idle":"2023-10-27T15:58:03.09239Z","shell.execute_reply.started":"2023-10-27T15:58:01.061912Z","shell.execute_reply":"2023-10-27T15:58:03.091262Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"show_images(cor,mask)","metadata":{"execution":{"iopub.status.busy":"2023-10-27T15:58:10.146342Z","iopub.execute_input":"2023-10-27T15:58:10.146915Z","iopub.status.idle":"2023-10-27T15:58:12.4989Z","shell.execute_reply.started":"2023-10-27T15:58:10.146883Z","shell.execute_reply":"2023-10-27T15:58:12.497961Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"show_images(cor)","metadata":{"execution":{"iopub.status.busy":"2023-10-27T15:58:12.500437Z","iopub.execute_input":"2023-10-27T15:58:12.500919Z","iopub.status.idle":"2023-10-27T15:58:14.298134Z","shell.execute_reply.started":"2023-10-27T15:58:12.50089Z","shell.execute_reply":"2023-10-27T15:58:14.297385Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"i = 8\nplt.imshow(cor[i])\nplt.imshow(mask[i], alpha=0.5, cmap='gray')","metadata":{"execution":{"iopub.status.busy":"2023-10-27T15:58:14.299276Z","iopub.execute_input":"2023-10-27T15:58:14.29973Z","iopub.status.idle":"2023-10-27T15:58:14.634364Z","shell.execute_reply.started":"2023-10-27T15:58:14.299703Z","shell.execute_reply":"2023-10-27T15:58:14.633273Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### 200+ images\n\nTry it again with more coronal slices to get more data","metadata":{}},{"cell_type":"code","source":"#Change this to get more or less images\ninterval = 2\nseries_path = create_series_path(df, train_dir, segmention_sample)\nfiltered_cor, _ = dicom_to_coronals(series_path, interval)","metadata":{"execution":{"iopub.status.busy":"2023-10-27T16:01:14.324338Z","iopub.execute_input":"2023-10-27T16:01:14.324734Z","iopub.status.idle":"2023-10-27T16:01:16.241135Z","shell.execute_reply.started":"2023-10-27T16:01:14.324703Z","shell.execute_reply":"2023-10-27T16:01:16.240139Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"#plot filtered coronal slices\nshow_images(filtered_cor)","metadata":{"execution":{"iopub.status.busy":"2023-10-27T16:01:20.42024Z","iopub.execute_input":"2023-10-27T16:01:20.42065Z","iopub.status.idle":"2023-10-27T16:01:29.565877Z","shell.execute_reply.started":"2023-10-27T16:01:20.420611Z","shell.execute_reply":"2023-10-27T16:01:29.564894Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Create video frames\n\nThe CT images could be approached as frames from a video. This function will take 24 coronal images and combine them into 8 frames.","metadata":{}},{"cell_type":"code","source":"# Save\ncoronal_dir = \"coronal\"\nif not os.path.exists(coronal_dir):\n    os.mkdir(coronal_dir)\nmasks_dir = \"masks\"\nif not os.path.exists(masks_dir):\n    os.mkdir(masks_dir)\n\n    \ndef save_frames(frames, masks, series_id):\n    for i,f in enumerate(frames):\n        f.save(f'{coronal_dir}/{series_id}_{i:02}.png')\n        cv2.imwrite(f'{masks_dir}/{series_id}_{i:02}.png', masks[i])","metadata":{"execution":{"iopub.status.busy":"2023-10-27T15:55:41.728542Z","iopub.status.idle":"2023-10-27T15:55:41.728914Z","shell.execute_reply.started":"2023-10-27T15:55:41.728739Z","shell.execute_reply":"2023-10-27T15:55:41.728756Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def get_frames(series_path, \n              interval = 5, \n              ignore_pixels = 25, \n              nonzero_threshold = 60,\n              num_frames = 16):\n    \"\"\"\n    interval - smaller yields more coronal images\n    ignore_pixels - ignore front and back dead space on CT\n    nonzero_threshold - higher values give more aggressive filtering\n    num_frames - number of 3 channel png to create\n    \"\"\"\n    #coronals\n    cor, mask = dicom_to_coronals(series_path, \n                              interval, \n                              ignore_pixels, \n                              nonzero_threshold,\n                              get_mask=True)\n    layers = 3\n    # count pixels for each coronal image\n    max_pixels = np.array([cor[i].sum() for i in range(len(cor))])\n    m = max_pixels.max()\n    distrib = np.array([round(i/m *100) for i in max_pixels])\n    # sort by size and take largest num_slices\n    num_slices = layers * num_frames\n    idx =  np.argsort(distrib)[-num_slices:]\n    idx.sort()\n    # stack is the filtered array of coronals\n    stack = cor[idx]\n    masks = mask[idx]\n    #if slices are < num_frames\n    n_slices = stack.shape[0]\n    if n_slices - 2 <= num_frames:\n        slice_idx = np.arange(n_slices)\n    else:\n        #evenly spaced slices\n        slice_idx = np.linspace(1, stack.shape[0]-1, num_frames, dtype=int)\n\n    masks = masks[slice_idx] \n    frames = []\n    for i in slice_idx:\n        arr = stack[i-1:i+2].swapaxes(0,2)\n        arr = arr[::-1,:,:]\n        arr = np.rot90(arr, 3, (0,1))\n        frames.append(Image.fromarray(arr))\n        \n    #save\n    series_id = str(series_path).split('/')[-1]\n    save_frames(frames, masks, series_id)","metadata":{"execution":{"iopub.status.busy":"2023-10-27T15:53:32.155467Z","iopub.execute_input":"2023-10-27T15:53:32.155908Z","iopub.status.idle":"2023-10-27T15:53:32.16805Z","shell.execute_reply.started":"2023-10-27T15:53:32.155875Z","shell.execute_reply":"2023-10-27T15:53:32.166866Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Look at a single saved frame","metadata":{}},{"cell_type":"code","source":"series_id  = 137\nseries_path = create_series_path(df, train_dir, series_id)\nget_frames(series_path)","metadata":{"execution":{"iopub.status.busy":"2023-10-27T16:05:11.028261Z","iopub.execute_input":"2023-10-27T16:05:11.028918Z","iopub.status.idle":"2023-10-27T16:05:32.574048Z","shell.execute_reply.started":"2023-10-27T16:05:11.028867Z","shell.execute_reply":"2023-10-27T16:05:32.573146Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"im = Image.open(f'/kaggle/working/coronal/{series_id}_04.png')\nplt.imshow(im)","metadata":{"execution":{"iopub.status.busy":"2023-10-27T16:05:32.576075Z","iopub.execute_input":"2023-10-27T16:05:32.576871Z","iopub.status.idle":"2023-10-27T16:05:32.904546Z","shell.execute_reply.started":"2023-10-27T16:05:32.576831Z","shell.execute_reply":"2023-10-27T16:05:32.903434Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]}]}