{"cells":[{"metadata":{},"cell_type":"markdown","source":"# The Idea\n\nI actually got this idea from another competition on Melanoma classification, from Roman's suggestion and implementation on the usage of advanced hair augmentation on skin images. So what I did to curate an insect (bee) dataset similar to what Roman did was manually download some bee pictures and blacked out the backgrounds in a paint software. You can find the dataset here: https://www.kaggle.com/khoongweihao/bee-augmentation.","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"# Training Notes\n\nFor EfficientNet training, I used Alex's notebook: https://www.kaggle.com/shonenkov/training-efficientdet. The weights for inference in this notebook are trained with similar parameters as Alex's (tweaked e.g. LR and resumed training at some epoch with different LR, etc), but with EfficientDet-D6 architecture. I trained the models locally on my trusty Nvidia AGX Xavier (32GB) overnight, which seems to be 1.5x sower than Kaggle's GPUs. The weights will not be made public, but it is easily obtainable by training using the same pipeline as I did. \n\nP.S. EfficientDet-D7 seems to perform poorly as compared to D5 and D6\n\n## Some Updates\n\n- Remark: > 0.74xx LB achievable with efficientdet (either d5, d6 or d7), but the best 0.74xx LB is **not** with insect augmentation!\n- version 10: added pseudo labeling (PL) to fold 0 weights","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"# Imports","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"import os\nimport cv2\nimport csv\nimport glob\nimport pandas as pd\nimport numpy as np\nimport random\nimport itertools\nfrom collections import Counter\nimport albumentations as A\nfrom math import ceil\nimport matplotlib.pyplot as plt\nfrom tqdm.notebook import tqdm\nfrom IPython.core.interactiveshell import InteractiveShell\nInteractiveShell.ast_node_interactivity = \"all\"\n%matplotlib inline","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# the following was referenced from reigHns notebooks at: https://www.kaggle.com/reighns/augmentations-data-cleaning-and-bounding-boxes\nimage_folder_path = \"/kaggle/input/global-wheat-detection/train/\"\nchosen_image = cv2.imread(os.path.join(image_folder_path, \"1ee6b9669.jpg\"))[:,:,::-1]\nplt.figure(figsize = (20,10))\nplt.imshow(chosen_image)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Insect Augmentation\n\nHere, we present the customized PyTorch Albumentations library transformation. Do read the **important remarks** below after the code and images!","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"from albumentations.core.transforms_interface import ImageOnlyTransform\n    \nclass InsectAugmentation(ImageOnlyTransform):\n    \"\"\"\n    Impose an image of a insect to the target image\n    -----------------------------------------------\n    \n    Author(s): Wei Hao Khoong\n    Built-upon Roman's AdvancedHairAugmentation in Melanoma competition\n    \n    Args:\n        insects (int): maximum number of insects to impose\n        insects_folder (str): path to the folder with insects images\n    \"\"\"\n\n    def __init__(self, insects=2, dark_insect=False, always_apply=False, p=0.5):\n        super().__init__(always_apply, p)\n        self.insects = insects\n        self.dark_insect = dark_insect\n        self.insects_folder = \"/kaggle/input/bee-augmentation/\"\n\n    def apply(self, image, **kwargs):\n        \"\"\"\n        Args:\n            image (PIL Image): Image to draw insects on.\n\n        Returns:\n            PIL Image: Image with drawn insects.\n        \"\"\"\n        n_insects = random.randint(1, self.insects) # for this example I put 1 instead of 0 to illustrate the augmentation\n        \n        if not n_insects:\n            return image\n        \n        height, width, _ = image.shape  # target image width and height\n        insects_images = [im for im in os.listdir(self.insects_folder) if 'png' in im]\n        \n        for _ in range(n_insects):\n            insect = cv2.cvtColor(cv2.imread(os.path.join(self.insects_folder, random.choice(insects_images))), cv2.COLOR_BGR2RGB)\n            insect = cv2.flip(insect, random.choice([-1, 0, 1]))\n            insect = cv2.rotate(insect, random.choice([0, 1, 2]))\n\n            h_height, h_width, _ = insect.shape  # insect image width and height\n            roi_ho = random.randint(0, image.shape[0] - insect.shape[0])\n            roi_wo = random.randint(0, image.shape[1] - insect.shape[1])\n            roi = image[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width]\n\n            # Creating a mask and inverse mask \n            img2gray = cv2.cvtColor(insect, cv2.COLOR_BGR2GRAY)\n            ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)\n            mask_inv = cv2.bitwise_not(mask)\n\n            # Now black-out the area of insect in ROI\n            img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)\n\n            # Take only region of insect from insect image.\n            if self.dark_insect:\n                img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)\n                insect_fg = cv2.bitwise_and(img_bg, img_bg, mask=mask)\n            else:\n                insect_fg = cv2.bitwise_and(insect, insect, mask=mask)\n\n            # Put insect in ROI and modify the target image\n            dst = cv2.add(img_bg, insect_fg, dtype=cv2.CV_64F)\n\n            image[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width] = dst\n                \n        return image ","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"img = InsectAugmentation(insects=2, always_apply=True)(image=chosen_image)['image']\nplt.figure(figsize = (20,10))\nplt.imshow(img)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"chosen_image = cv2.imread(os.path.join(image_folder_path, \"1ee6b9669.jpg\"))[:,:,::-1]\nimg = InsectAugmentation(insects=2, dark_insect=True, always_apply=True)(image=chosen_image)['image']\nplt.figure(figsize = (20,10))\nplt.imshow(img)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Important Remarks\n\n- if you decide to create your own customized insect dataset, do bear in mind **not to have very large insects** as they will obscure wheat heads! I tired it at first (with bees half as large as a wheat head and it was a disaster during training)\n- **do not set too high a value for the hyperparameter `insects`** as having too many insects will also obscure important information in the images like the wheat heads\n- i found that `insects=1` or `insects=2` works best so far on EfficientDet architectures. I used EfficientDet-D6 here only as an example. Feel free to experiement with other EfficientDet architectures or models like Faster R-CNN, Detectron2.","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"# WBF approach over TTA for single model EfficientDet (Inference)\n\nFirstly, a big thanks to Alex for sharing his great notebooks in the competition, and in others too! :) I've learnt a lot, and I'm sure many others have as well. The rest of the notebook are from Alex's notebooks. Some links to his great work for GWD can be found here:\n\n- [WBF approach for ensemble](https://www.kaggle.com/shonenkov/wbf-approach-for-ensemble)\n- [[Training] EfficientDet](https://www.kaggle.com/shonenkov/training-efficientdet)\n- [[Inference] EfficientDet](https://www.kaggle.com/shonenkov/inference-efficientdet)\n- [[OOF-Evaluation][Mixup] EfficientDet](https://www.kaggle.com/shonenkov/oof-evaluation-mixup-efficientdet)\n- [[Bayesian optimization WBF] EfficientDet](https://www.kaggle.com/shonenkov/bayesian-optimization-wbf-efficientdet)","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"## Main Idea (Quoted from Alex)\n\nToday I would like to share with you TTA approach for object detection tasks. \nTTA (Test Time Augmentation) is approach with augmentation of test images. I have created custom TTA API with clear understanding! You can create own TTA approaches using my examples)\n\nFor prediction I would like to use single model, that I published earlier.\n\nFor ensemble of TTA I would like to use \"best of the best\" WBF! Author of WBF is really cool russian competitions grandmaster [Roman Solovyev @ZFturbo](https://www.kaggle.com/zfturbo)! \n\n### If you like his work about WBF, please, add star [github repo](https://github.com/ZFTurbo/Weighted-Boxes-Fusion)!","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"## Dependencies","execution_count":null},{"metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_kg_hide-input":true,"_kg_hide-output":true,"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","trusted":true},"cell_type":"code","source":"!pip install --no-deps '../input/timm-package/timm-0.1.26-py3-none-any.whl' > /dev/null\n!pip install --no-deps '../input/pycocotools/pycocotools-2.0-cp37-cp37m-linux_x86_64.whl' > /dev/null","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"trusted":true},"cell_type":"code","source":"import sys\nsys.path.insert(0, \"../input/timm-efficientdet-pytorch\")\nsys.path.insert(0, \"../input/omegaconf\")\nsys.path.insert(0, \"../input/weightedboxesfusion\")\n\nimport ensemble_boxes\nimport torch\nimport numpy as np\nimport pandas as pd\nfrom glob import glob\nfrom torch.utils.data import Dataset,DataLoader\nimport albumentations as A\nfrom albumentations.pytorch.transforms import ToTensorV2\nimport cv2\nimport gc\nfrom matplotlib import pyplot as plt\nfrom effdet import get_efficientdet_config, EfficientDet, DetBenchEval\nfrom effdet.efficientdet import HeadNet","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def get_valid_transforms():\n    return A.Compose([\n            A.Resize(height=512, width=512, p=1.0),\n            ToTensorV2(p=1.0),\n        ], p=1.0)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"DATA_ROOT_PATH = '../input/global-wheat-detection/test'\n\nclass DatasetRetriever(Dataset):\n\n    def __init__(self, image_ids, transforms=None):\n        super().__init__()\n        self.image_ids = image_ids\n        self.transforms = transforms\n\n    def __getitem__(self, index: int):\n        image_id = self.image_ids[index]\n        image = cv2.imread(f'{DATA_ROOT_PATH}/{image_id}.jpg', cv2.IMREAD_COLOR)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.float32)\n        image /= 255.0\n        if self.transforms:\n            sample = {'image': image}\n            sample = self.transforms(**sample)\n            image = sample['image']\n        return image, image_id\n\n    def __len__(self) -> int:\n        return self.image_ids.shape[0]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"dataset = DatasetRetriever(\n    image_ids=np.array([path.split('/')[-1][:-4] for path in glob(f'{DATA_ROOT_PATH}/*.jpg')]),\n    transforms=get_valid_transforms()\n)\n\ndef collate_fn(batch):\n    return tuple(zip(*batch))\n\ndata_loader = DataLoader(\n    dataset,\n    batch_size=4,\n    shuffle=False,\n    num_workers=2,\n    drop_last=False,\n    collate_fn=collate_fn\n)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"!ls ../input/gwd-efficientdetd6-weights/","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def load_net(checkpoint_path):\n    config = get_efficientdet_config('tf_efficientdet_d6')\n    net = EfficientDet(config, pretrained_backbone=False)\n\n    config.num_classes = 1\n    config.image_size=512\n    net.class_net = HeadNet(config, num_outputs=config.num_classes, norm_kwargs=dict(eps=.001, momentum=.01))\n\n    checkpoint = torch.load(checkpoint_path)\n    net.load_state_dict(checkpoint['model_state_dict'])\n    #net.load_state_dict(checkpoint)\n\n    del checkpoint\n    gc.collect()\n\n    net = DetBenchEval(net, config)\n    net.eval();\n    return net.cuda()\n\nnet = load_net('../input/gwd-efficientdetd6-weights/fold0-efficientdetd6-PL-last-checkpoint1.bin')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Custom TTA API\n\nIdea is simple: \n- `augment` make tta for one image\n- `batch_augment` make tta for batch of images\n- `deaugment_boxes` return tta predicted boxes in back to original state of image\n\nAlso we are interested in `Compose` with combinations of tta :)","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"class BaseWheatTTA:\n    \"\"\" author: @shonenkov \"\"\"\n    image_size = 512\n\n    def augment(self, image):\n        raise NotImplementedError\n    \n    def batch_augment(self, images):\n        raise NotImplementedError\n    \n    def deaugment_boxes(self, boxes):\n        raise NotImplementedError\n\nclass TTAHorizontalFlip(BaseWheatTTA):\n    \"\"\" author: @shonenkov \"\"\"\n\n    def augment(self, image):\n        return image.flip(1)\n    \n    def batch_augment(self, images):\n        return images.flip(2)\n    \n    def deaugment_boxes(self, boxes):\n        boxes[:, [1,3]] = self.image_size - boxes[:, [3,1]]\n        return boxes\n\nclass TTAVerticalFlip(BaseWheatTTA):\n    \"\"\" author: @shonenkov \"\"\"\n    \n    def augment(self, image):\n        return image.flip(2)\n    \n    def batch_augment(self, images):\n        return images.flip(3)\n    \n    def deaugment_boxes(self, boxes):\n        boxes[:, [0,2]] = self.image_size - boxes[:, [2,0]]\n        return boxes\n    \nclass TTARotate90(BaseWheatTTA):\n    \"\"\" author: @shonenkov \"\"\"\n    \n    def augment(self, image):\n        return torch.rot90(image, 1, (1, 2))\n\n    def batch_augment(self, images):\n        return torch.rot90(images, 1, (2, 3))\n    \n    def deaugment_boxes(self, boxes):\n        res_boxes = boxes.copy()\n        res_boxes[:, [0,2]] = self.image_size - boxes[:, [1,3]]\n        res_boxes[:, [1,3]] = boxes[:, [2,0]]\n        return res_boxes\n    \nclass TTARotate180(BaseWheatTTA):\n    \"\"\" author: @shonenkov \"\"\"\n    \n    def augment(self, image):\n        return torch.rot90(image, 2, (1, 2))\n\n    def batch_augment(self, images):\n        return torch.rot90(images, 2, (2, 3))\n    \n    def deaugment_boxes(self, boxes):\n        boxes[:, [0,1,2,3]] = self.image_size - boxes[:, [2,3,0,1]]\n        return boxes\n    \nclass TTARotate270(BaseWheatTTA):\n    \"\"\" author: @shonenkov \"\"\"\n    \n    def augment(self, image):\n        return torch.rot90(image, 3, (1, 2))\n\n    def batch_augment(self, images):\n        return torch.rot90(images, 3, (2, 3))\n    \n    def deaugment_boxes(self, boxes):\n        res_boxes = boxes.copy()\n        res_boxes[:, [0,2]] = boxes[:, [1,3]]\n        res_boxes[:, [1,3]] = self.image_size - boxes[:, [2,0]]\n        return res_boxes\n    \nclass TTACompose(BaseWheatTTA):\n    \"\"\" author: @shonenkov \"\"\"\n    def __init__(self, transforms):\n        self.transforms = transforms\n        \n    def augment(self, image):\n        for transform in self.transforms:\n            image = transform.augment(image)\n        return image\n    \n    def batch_augment(self, images):\n        for transform in self.transforms:\n            images = transform.batch_augment(images)\n        return images\n    \n    def prepare_boxes(self, boxes):\n        result_boxes = boxes.copy()\n        result_boxes[:,0] = np.min(boxes[:, [0,2]], axis=1)\n        result_boxes[:,2] = np.max(boxes[:, [0,2]], axis=1)\n        result_boxes[:,1] = np.min(boxes[:, [1,3]], axis=1)\n        result_boxes[:,3] = np.max(boxes[:, [1,3]], axis=1)\n        return result_boxes\n    \n    def deaugment_boxes(self, boxes):\n        for transform in self.transforms[::-1]:\n            boxes = transform.deaugment_boxes(boxes)\n        return self.prepare_boxes(boxes)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Demonstration how it works","execution_count":null},{"metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"trusted":true},"cell_type":"code","source":"def process_det(index, det, score_threshold=0.33):\n    boxes = det[index].detach().cpu().numpy()[:,:4]    \n    scores = det[index].detach().cpu().numpy()[:,4]\n    boxes[:, 2] = boxes[:, 2] + boxes[:, 0]\n    boxes[:, 3] = boxes[:, 3] + boxes[:, 1]\n    boxes = (boxes).clip(min=0, max=511).astype(int)\n    indexes = np.where(scores>score_threshold)\n    boxes = boxes[indexes]\n    scores = scores[indexes]\n    return boxes, scores","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# you can try own combinations:\ntransform = TTACompose([\n    TTARotate90(),\n    TTAVerticalFlip(),\n])\n\nfig, ax = plt.subplots(1, 3, figsize=(16, 6))\n\nimage, image_id = dataset[5]\n\nnumpy_image = image.permute(1,2,0).cpu().numpy().copy()\n\nax[0].imshow(numpy_image);\nax[0].set_title('original')\n\ntta_image = transform.augment(image)\ntta_image_numpy = tta_image.permute(1,2,0).cpu().numpy().copy()\n\ndet = net(tta_image.unsqueeze(0).float().cuda(), torch.tensor([1]).float().cuda())\nboxes, scores = process_det(0, det)\n\nfor box in boxes:\n    cv2.rectangle(tta_image_numpy, (box[0], box[1]), (box[2],  box[3]), (0, 1, 0), 2)\n\nax[1].imshow(tta_image_numpy);\nax[1].set_title('tta')\n    \nboxes = transform.deaugment_boxes(boxes)\n\nfor box in boxes:\n    cv2.rectangle(numpy_image, (box[0], box[1]), (box[2],  box[3]), (0, 1, 0), 2)\n    \nax[2].imshow(numpy_image);\nax[2].set_title('deaugment predictions');","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Combinations of TTA","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"from itertools import product\n\ntta_transforms = []\nfor tta_combination in product([TTAHorizontalFlip(), TTAVerticalFlip(), None],\n                               [TTARotate90(), TTARotate180(), TTARotate270(), None]):\n    tta_transforms.append(TTACompose([tta_transform for tta_transform in tta_combination if tta_transform]))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## WBF over TTA","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def make_tta_predictions(images, score_threshold=0.33):\n    with torch.no_grad():\n        images = torch.stack(images).float().cuda()\n        predictions = []\n        for tta_transform in tta_transforms:\n            result = []\n            det = net(tta_transform.batch_augment(images.clone()), torch.tensor([1]*images.shape[0]).float().cuda())\n\n            for i in range(images.shape[0]):\n                boxes = det[i].detach().cpu().numpy()[:,:4]    \n                scores = det[i].detach().cpu().numpy()[:,4]\n                indexes = np.where(scores > score_threshold)[0]\n                boxes = boxes[indexes]\n                boxes[:, 2] = boxes[:, 2] + boxes[:, 0]\n                boxes[:, 3] = boxes[:, 3] + boxes[:, 1]\n                boxes = tta_transform.deaugment_boxes(boxes.copy())\n                result.append({\n                    'boxes': boxes,\n                    'scores': scores[indexes],\n                })\n            predictions.append(result)\n    return predictions\n\ndef run_wbf(predictions, image_index, image_size=512, iou_thr=0.44, skip_box_thr=0.43, weights=None):\n    boxes = [(prediction[image_index]['boxes']/(image_size-1)).tolist() for prediction in predictions]\n    scores = [prediction[image_index]['scores'].tolist() for prediction in predictions]\n    labels = [np.ones(prediction[image_index]['scores'].shape[0]).astype(int).tolist() for prediction in predictions]\n    boxes, scores, labels = ensemble_boxes.ensemble_boxes_wbf.weighted_boxes_fusion(boxes, scores, labels, weights=None, iou_thr=iou_thr, skip_box_thr=skip_box_thr)\n    boxes = boxes*(image_size-1)\n    return boxes, scores, labels","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"import matplotlib.pyplot as plt\n\nfor j, (images, image_ids) in enumerate(data_loader):\n    break\n\npredictions = make_tta_predictions(images)\n\ni = 1\nsample = images[i].permute(1,2,0).cpu().numpy()\n\nboxes, scores, labels = run_wbf(predictions, image_index=i)\nboxes = boxes.round().astype(np.int32).clip(min=0, max=511)\n\nfig, ax = plt.subplots(1, 1, figsize=(16, 8))\n\nfor box in boxes:\n    cv2.rectangle(sample, (box[0], box[1]), (box[2], box[3]), (1, 0, 0), 1)\n\nax.set_axis_off()\nax.imshow(sample);","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Inference","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def format_prediction_string(boxes, scores):\n    pred_strings = []\n    for j in zip(scores, boxes):\n        pred_strings.append(\"{0:.4f} {1} {2} {3} {4}\".format(j[0], j[1][0], j[1][1], j[1][2], j[1][3]))\n    return \" \".join(pred_strings)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"results = []\n\nfor images, image_ids in data_loader:\n    predictions = make_tta_predictions(images)\n    for i, image in enumerate(images):\n        boxes, scores, labels = run_wbf(predictions, image_index=i)\n        boxes = (boxes*2).round().astype(np.int32).clip(min=0, max=1023)\n        image_id = image_ids[i]\n\n        boxes[:, 2] = boxes[:, 2] - boxes[:, 0]\n        boxes[:, 3] = boxes[:, 3] - boxes[:, 1]\n\n        result = {\n            'image_id': image_id,\n            'PredictionString': format_prediction_string(boxes, scores)\n        }\n        results.append(result)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"test_df = pd.DataFrame(results, columns=['image_id', 'PredictionString'])\ntest_df.to_csv('submission.csv', index=False)\ntest_df.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"#  References\n\nSome references to notebooks were made in this notebook:\n- https://www.kaggle.com/reighns/augmentations-data-cleaning-and-bounding-boxes\n- https://www.kaggle.com/shonenkov/wbf-over-tta-single-model-efficientdet\n- https://www.kaggle.com/c/siim-isic-melanoma-classification/discussion/159176\n- https://www.kaggle.com/c/siim-isic-melanoma-classification/discussion/159476","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"## Thanks for reading and happy Kaggling!","execution_count":null}],"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":4,"nbformat_minor":4}