{"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":"This notebook is work-in-progress and expected to be updated.\n\nCredits to other notebooks:\n* [Visual In-Depth EDA – VinBigData Competition Data](https://www.kaggle.com/dschettler8845/visual-in-depth-eda-vinbigdata-competition-data) by @dschettler8845\n* [SIIM COVID-19 Detection - a simple EDA 🦠🩺](https://www.kaggle.com/tanlikesmath/siim-covid-19-detection-a-simple-eda) by @tanlikesmath","metadata":{}},{"cell_type":"markdown","source":"# Load Libraries and Data","metadata":{}},{"cell_type":"code","source":"from ast import literal_eval\nimport cv2\nfrom fastai.vision.all import *\nfrom fastai.medical.imaging import *\nfrom matplotlib import pyplot as plt\nimport numpy as np # linear algebra\nimport os\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\nfrom pathlib import Path\nimport pydicom\nfrom pydicom.pixel_data_handlers.util import apply_voi_lut\nimport seaborn as sns\nfrom warnings import simplefilter","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"plt.style.use('fivethirtyeight')\nsimplefilter('ignore')","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def dicom2array(path, voi_lut=True, fix_monochrome=True):\n    \"\"\" Convert dicom file to numpy array \n    \n    Args:\n        path (str): Path to the dicom file to be converted\n        voi_lut (bool): Whether or not VOI LUT is available\n        fix_monochrome (bool): Whether or not to apply monochrome fix\n        \n    Returns:\n        Numpy array of the respective dicom file \n        \n    \"\"\"\n    # Use the pydicom library to read the dicom file\n    dicom = pydicom.read_file(path)\n    \n    # VOI LUT (if available by DICOM device) is used to \n    # transform raw DICOM data to \"human-friendly\" view\n    if voi_lut:\n        data = apply_voi_lut(dicom.pixel_array, dicom)\n    else:\n        data = dicom.pixel_array\n        \n    # The XRAY may look inverted\n    #   - If we want to fix this we can\n    if fix_monochrome and dicom.PhotometricInterpretation == \"MONOCHROME1\":\n        data = np.amax(data) - data\n    \n    # Normalize the image array and return\n    data = data - np.min(data)\n    data = data / np.max(data)\n    data = (data * 255).astype(np.uint8)\n    return data\n\ndef plot_imgs(imgs, cols=4, size=7, is_rgb=True, title=\"\", cmap='gray', img_size=(500,500)):\n    rows = len(imgs)//cols + 1\n    fig = plt.figure(figsize=(cols*size, rows*size))\n    for i, img in enumerate(imgs):\n        if img_size is not None:\n            img = cv2.resize(img, img_size)\n        fig.add_subplot(rows, cols, i+1)\n        plt.imshow(img, cmap=cmap)\n    plt.suptitle(title)\n    plt.show()\n    \ndef get_image_id(path):\n    \"\"\" Function to return the image-id from a path \"\"\"\n    return path.rsplit(\"/\", 1)[1].rsplit(\".\", 1)[0]\n\ndef create_fractional_bbox_coordinates(row):\n    \"\"\" Function to return bbox coordiantes as fractions from DF row \"\"\"\n    frac_x_min = row[\"x_min\"]/row[\"img_width\"]\n    frac_x_max = row[\"x_max\"]/row[\"img_width\"]\n    frac_y_min = row[\"y_min\"]/row[\"img_height\"]\n    frac_y_max = row[\"y_max\"]/row[\"img_height\"]\n    return frac_x_min, frac_x_max, frac_y_min, frac_y_max\n\ndef draw_bboxes(img, tl, br, rgb, label=\"\", label_location=\"tl\", opacity=0.1, line_thickness=0):\n    \"\"\" TBD \n    \n    Args:\n        TBD\n        \n    Returns:\n        TBD \n    \"\"\"\n    rect = np.uint8(np.ones((br[1]-tl[1], br[0]-tl[0], 3))*rgb)\n    sub_combo = cv2.addWeighted(img[tl[1]:br[1],tl[0]:br[0],:], 1-opacity, rect, opacity, 1.0)    \n    img[tl[1]:br[1],tl[0]:br[0],:] = sub_combo\n\n    if line_thickness>0:\n        img = cv2.rectangle(img, tuple(tl), tuple(br), rgb, line_thickness)\n        \n    if label:\n        # DEFAULTS\n        FONT = cv2.FONT_HERSHEY_SIMPLEX\n        FONT_SCALE = 1.666\n        FONT_THICKNESS = 3\n        FONT_LINE_TYPE = cv2.LINE_AA\n        \n        if type(label)==str:\n            LABEL = label.upper().replace(\" \", \"_\")\n        else:\n            LABEL = f\"CLASS_{label:02}\"\n        \n        text_width, text_height = cv2.getTextSize(LABEL, FONT, FONT_SCALE, FONT_THICKNESS)[0]\n        \n        label_origin = {\"tl\":tl, \"br\":br, \"tr\":(br[0],tl[1]), \"bl\":(tl[0],br[1])}[label_location]\n        label_offset = {\n            \"tl\":np.array([0, -10]), \"br\":np.array([-text_width, text_height+10]), \n            \"tr\":np.array([-text_width, -10]), \"bl\":np.array([0, text_height+10])\n        }[label_location]\n        img = cv2.putText(img, LABEL, tuple(label_origin+label_offset), \n                          FONT, FONT_SCALE, rgb, FONT_THICKNESS, FONT_LINE_TYPE)\n    \n    return img","metadata":{"_kg_hide-input":true,"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"class TrainData():\n    def __init__(self, df, train_dir, cmap=\"Spectral\"):\n        # Initialize\n        self.df = df\n        self.train_dir = train_dir\n        \n        # Visualization\n        self.cmap = cmap\n        self.pal = [tuple([int(x) for x in np.array(c)*(255,255,255)]) for c in sns.color_palette(cmap, 15)]\n        self.pal.pop(8)\n        \n        # Store df components in individual numpy arrays for easy access based on index\n        tmp_numpy = self.df.to_numpy()\n        image_ids = self.df.index.values\n        class_ids = tmp_numpy[1]\n        rad_ids = tmp_numpy[2]\n        bboxes = tmp_numpy[3:]\n        \n        self.img_annotations = self.get_annotations(get_all=True)\n        \n        # Clean-Up\n        del tmp_numpy; gc.collect();\n        \n        \n    def get_annotations(self, get_all=False, image_ids=None, class_ids=None, rad_ids=None, index=None):\n        \"\"\" TBD \n        \n        Args:\n            get_all (bool, optional): TBD\n            image_ids (list of strs, optional): TBD\n            class_ids (list of ints, optional): TBD\n            rad_ids (list of strs, optional): TBD\n            index (int, optional):\n        \n        Returns:\n        \n        \n        \"\"\"\n        if not get_all and image_ids is None and class_ids is None and rad_ids is None and index is None:\n            raise ValueError(\"Expected one of the following arguments to be passed:\" \\\n                             \"\\n\\t\\t– `get_all`, `image_id`, `class_id`, `rad_id`, or `index`\")\n        # Initialize\n        tmp_df = self.df.copy()\n        \n        if not get_all:\n            if image_ids is not None:\n                tmp_df = tmp_df[tmp_df.image_id.isin(image_ids)]\n            if class_ids is not None:\n                tmp_df = tmp_df[tmp_df.class_id.isin(class_ids)]\n            if rad_ids is not None:\n                tmp_df = tmp_df[tmp_df.rad_id.isin(rad_ids)]\n            if index is not None:\n                tmp_df = tmp_df.iloc[index]\n            \n        annotations = {image_id:[] for image_id in tmp_df.image_id.to_list()}\n        for row in tmp_df.to_numpy():\n            \n            # Update annotations dictionary\n            annotations[row[0]].append(dict(\n                img_path=os.path.join(self.train_dir, row[0]+\".dicom\"),\n                image_id=row[0],\n                class_id=int(row[1]),\n                rad_id=int(row[2][1:]),\n            ))\n            \n            # Catch to convert float array to integer array\n            if row[1]==14:\n                annotations[row[0]][-1][\"bbox\"]=row[3:]\n            else:\n                annotations[row[0]][-1][\"bbox\"]=row[3:].astype(np.int32)\n        return annotations\n    \n    def get_annotated_image(self, image_id, annots=None, plot=False, plot_size=(18,25), plot_title=\"\"):\n        if annots is None:\n            annots = self.img_annotations.copy()\n        \n        if type(annots) != list:\n            image_annots = annots[image_id]\n        else:\n            image_annots = annots\n            \n        img = cv2.cvtColor(dicom2array(image_annots[0][\"img_path\"]),cv2.COLOR_GRAY2RGB)\n        for ann in image_annots:\n            if ann[\"class_id\"] != 14:\n                img = draw_bboxes(img, \n                                ann[\"bbox\"][:2], ann[\"bbox\"][-2:], \n                                rgb=self.pal[ann[\"class_id\"]], \n                                label=int_2_str[ann[\"class_id\"]], \n                                opacity=0.08, line_thickness=4)\n        if plot:\n            plot_image(img, title=plot_title, figsize=plot_size)\n        \n        return img\n    \n    def plot_image_ids(self, image_id_list, height_multiplier=6, verbose=True):\n        annotations = self.get_annotations(image_ids=image_id_list)\n        annotated_imgs = []\n        n = len(image_id_list)\n        \n        plt.figure(figsize=(20, height_multiplier*n))\n        for i, (image_id, annots) in enumerate(annotations.items()):\n            if i >= n:\n                break\n            if verbose:\n                print(f\".\", end=\"\")\n            plt.subplot(n//2,2,i+1)\n            plt.imshow(self.get_annotated_image(image_id, annots))\n            plt.axis(False)\n            plt.title(f\"Image ID – {image_id}\")\n        plt.tight_layout(rect=[0, 0.03, 1, 0.97])\n        plt.show()\n        \n    def plot_classes(self, class_list, n=4, height_multiplier=6, verbose=True):\n        annotations = self.get_annotations(class_ids=class_list)\n        annotated_imgs = []\n\n        plt.figure(figsize=(20, height_multiplier*n))\n        for i, (image_id, annots) in enumerate(annotations.items()):\n            if i >= n:\n                break\n            if verbose:\n                print(f\".\", end=\"\")\n            plt.subplot(n//2,2,i+1)\n            plt.imshow(self.get_annotated_image(image_id, annots))\n            plt.axis(False)\n            plt.title(f\"Image ID – {image_id}\")\n        plt.tight_layout(rect=[0, 0.03, 1, 0.97])\n        plt.show()\n\n    def plot_radiologists(self, rad_id_list, n=4, height_multiplier=6, verbose=True):\n        annotations = self.get_annotations(rad_ids=rad_id_list)\n        annotated_imgs = []\n\n        plt.figure(figsize=(20, height_multiplier*n))\n        for i, (image_id, annots) in enumerate(annotations.items()):\n            if i >= n:\n                break\n            if verbose:\n                print(f\".\", end=\"\")\n            plt.subplot(n//2,2,i+1)\n            plt.imshow(self.get_annotated_image(image_id, annots))\n            plt.axis(False)\n            plt.title(f\"Image ID – {image_id}\")\n        plt.tight_layout(rect=[0, 0.03, 1, 0.97])\n        plt.show()","metadata":{"_kg_hide-input":true,"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"data_dir = Path('../input/siim-covid19-detection')\ntrain_image_level_file = data_dir / 'train_image_level.csv'\ntrain_study_level_file = data_dir / 'train_study_level.csv'\nsample_file = data_dir / 'sample_submission.csv'\n\nbuild_dir = Path('./build')\nbuild_dir.mkdir(parents=True, exist_ok=True)\nsubmission_file = 'submission.csv'\n\nid_col = 'id'\nimage_label_col = 'label'\nimage_class_col = 'image_class'\nbbox_col = 'boxes'","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"trn_image = pd.read_csv(train_image_level_file)\nprint(trn_image.shape)\ntrn_image.head()","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"trn_study = pd.read_csv(train_study_level_file)\nprint(trn_study.shape)\ntrn_study.head()","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# EDA","metadata":{}},{"cell_type":"markdown","source":"## Image Level Label","metadata":{}},{"cell_type":"code","source":"trn_image[bbox_col][0]","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"trn_image[image_label_col][0]","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"The `boxes` and `label` columns are encoded as `str`, and we need to convert them to `list`.","metadata":{}},{"cell_type":"code","source":"trn_image[bbox_col] = trn_image[bbox_col].apply(lambda x: literal_eval(x) if not pd.isnull(x) else [])\nprint(trn_image[bbox_col].apply(len).describe())\ntrn_image[bbox_col].apply(len).hist(bins=10, width=.2)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"print(trn_image[image_label_col].str.split(' ').str[0].value_counts())\ntrn_image[image_label_col].str.split(' ').str[0].hist(bins=2, width=.2)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"trn_image[image_label_col] = trn_image[image_label_col].str.split(' ').str[::6]\ntrn_image[image_label_col].head()","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"trn_bbox = pd.concat([trn_image[[bbox_col]].reset_index().explode(bbox_col), \n                      trn_image[[image_label_col]].reset_index(drop=True).explode(image_label_col)], axis=1)\nprint(trn_bbox.shape)\ntrn_bbox.head(10)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"print(trn_bbox[image_label_col].value_counts())\ntrn_bbox[image_label_col].hist(bins=2, width=.2)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Looks good! We have 7,853 bounding boxes from 4,294 images labeled as `opacity`. ","metadata":{}},{"cell_type":"markdown","source":"## Study Level Label","metadata":{}},{"cell_type":"markdown","source":"Next, let's look at the study level data.","metadata":{}},{"cell_type":"markdown","source":"There are four classes: `negative`, `typical`, `indeterminate`, and `atypical`. Let's check their distributions.","metadata":{}},{"cell_type":"code","source":"trn_study.describe()","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Let's check whether it's multiclass (classes are mutually exclusive) or multilabel (classes can overlap).","metadata":{}},{"cell_type":"code","source":"trn_study.sum(axis=1).describe()","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"It's multiclass. i.e. each sample (study) belongs to only one class.","metadata":{}},{"cell_type":"markdown","source":"## Images","metadata":{}},{"cell_type":"code","source":"dicom_paths = get_dicom_files(data_dir /'train')\nimgs = [dicom2array(path) for path in dicom_paths[:4]]\nplot_imgs(imgs)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"To be continued","metadata":{}},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]}]}