{"cells":[{"metadata":{"trusted":true},"cell_type":"code","source":"#!cp -r ../input/maskrcnn-keras-source-code/MaskRCNN/* ./","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\nimport glob\nimport os\n#for dirname, _, filenames in os.walk('/kaggle/input'):\n  #  for filename in filenames:\n  #      print(os.path.join(dirname, filename))\n\n# You can write up to 5GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Starter code is from https://www.kaggle.com/pednt9/gwd-keras-unet-starter. Submission is corrected. Splitted maskes and data augmentation are added","execution_count":null},{"metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","trusted":true},"cell_type":"code","source":"import os\nimport sys\nimport random\nimport warnings\n#from mrcnn import utils\nimport numpy as np\nimport pandas as pd\nimport geopandas as gpd\nimport matplotlib.pyplot as plt\nimport shapely\nfrom shapely.geometry import LineString, shape\nfrom itertools import chain\nfrom skimage.io import imread, imshow, imread_collection, concatenate_images\nfrom skimage.transform import resize\nfrom skimage.measure import label, regionprops\nfrom PIL import Image, ImageDraw\nfrom ast import literal_eval\nfrom tqdm.notebook import tqdm\nimport keras\nfrom shapely.geometry import LineString, shape, Polygon\nfrom keras import backend as K\n#from keras.preprocessing.image import ImageDataGenerator\nfrom keras.models import Model, load_model\nfrom keras.layers import Input, BatchNormalization, Activation, Dense, Dropout\nfrom keras.layers.core import Dropout, Lambda\nfrom keras.layers.convolutional import Conv2D, Conv2DTranspose\nfrom keras.layers.pooling import MaxPooling2D\nfrom keras.layers.merge import concatenate\nfrom keras.callbacks import EarlyStopping, ModelCheckpoint\nfrom keras.preprocessing.image import ImageDataGenerator\nfrom keras.optimizers import Adam\nfrom imgaug import augmenters as iaa\nimport tensorflow as tf\nfrom imgaug import augmenters as iaa\n# Set some parameters\nIMG_WIDTH = 256\nIMG_HEIGHT = 256\nassert IMG_WIDTH == IMG_HEIGHT\nBATCH_SIZE = 32 # the higher the better\nIMG_CHANNELS = 3\nTRAIN_PATH = '/kaggle/input/global-wheat-detection/train/'\nTEST_PATH = '/kaggle/input/global-wheat-detection/test/'\nSC_FACTOR = int(1024 / IMG_WIDTH)\nimport sklearn\nfrom sklearn.model_selection import train_test_split\n\nwarnings.filterwarnings('ignore')\nseed = 42\n#random.seed = seed\n#np.random.seed = seed","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"PATH = \"../input/global-wheat-detection/\"\ntrain_folder = os.path.join(PATH, \"train\")\ntest_folder = os.path.join(PATH, \"test\")\n\ntrain_csv_path = os.path.join(PATH, \"train.csv\")\ndf = pd.read_csv(train_csv_path)\nsample_sub = pd.read_csv(PATH + \"sample_submission.csv\")\n\ndf.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Get train and test IDs and paths\ntrain_ids = os.listdir(TRAIN_PATH)\ntest_ids = os.listdir(TEST_PATH)\nprint(test_ids)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def slice_box(box_A:Polygon, box_B:Polygon, margin=10, line_mult=10):\n    \"Returns box_A sliced according to the distance to box_B.\"\n    vec_AB = np.array([box_B.centroid.x - box_A.centroid.x, box_B.centroid.y - box_A.centroid.y])\n    vec_ABp = np.array([-(box_B.centroid.y - box_A.centroid.y), box_B.centroid.x - box_A.centroid.x])\n    vec_AB_norm = np.linalg.norm(vec_AB)\n    split_point = box_A.centroid + vec_AB/2 - (vec_AB/vec_AB_norm)*margin\n    line = LineString([split_point-line_mult*vec_ABp, split_point+line_mult*vec_ABp])\n    split_box = shapely.ops.split(box_A, line)\n    if len(split_box) == 1: return split_box, None, line\n    is_center = [s.contains(box_A.centroid) for s in split_box]\n    if sum(is_center) == 0: \n        warnings.warn('Polygon do not contain the center of original box, keeping the first slice.')\n        return split_box[0], None, line\n    where_is_center = np.argwhere(is_center).reshape(-1)[0]\n    where_not_center = np.argwhere(~np.array(is_center)).reshape(-1)[0]\n    split_box_center = split_box[where_is_center]\n    split_box_out = split_box[where_not_center]\n    return split_box_center, split_box_out, line\ndef intersection_list(polylist):\n    r = polylist[0]\n    for p in polylist:\n        r = r.intersection(p)\n    return r\n    \ndef slice_one(gdf, index):\n    inter = gdf.loc[gdf.intersects(gdf.iloc[index].geometry)]\n    if len(inter) == 1: return inter.geometry.values[0]\n    box_A = inter.loc[index].values[0]\n    inter = inter.drop(index, axis=0)\n    polys = []\n    for i in range(len(inter)):\n        box_B = inter.iloc[i].values[0]\n        polyA, *_ = slice_box(box_A, box_B)\n        polys.append(polyA)\n    return intersection_list(polys)\n\ndef slice_all(gdf):\n    polys = []\n    for i in range(len(gdf)):\n        polys.append(slice_one(gdf, i))\n    return gpd.GeoDataFrame({'geometry': polys})","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"import io\nfrom itertools import chain\ndef make_polygon(coords):\n    xm, ym, w, h = coords\n    xm, ym, w, h = xm / SC_FACTOR, ym / SC_FACTOR, w / SC_FACTOR, h / SC_FACTOR\n    #polygon = , [(xm, ym), (xm, ym + h)(xm + w, ym + h), (xm + w, ym)]\n    return Polygon([(xm, ym), (xm, ym + h), (xm + w, ym + h), (xm + w, ym)])\n\nmasks = dict() # dictionnary containing all masks\n\nfor img_id, gp in tqdm(df.groupby(\"image_id\")):\n    gp['polygons'] = gp['bbox'].apply(eval).apply(lambda x: make_polygon(x))\n    img = Image.new('L', (IMG_WIDTH, IMG_HEIGHT), 0)    \n    gdf = gpd.GeoDataFrame({'geometry': list(gp['polygons'])})\n    #print(gdf.head())\n    dfg=slice_all(gdf)\n    g = [i for i in dfg.geometry]\n    img = Image.new('L', (IMG_WIDTH, IMG_HEIGHT), 0)\n    for i in g:\n        line_new=i.boundary\n        pol=list(shape(line_new).coords)\n        #print(pol)\n        ImageDraw.Draw(img).polygon(pol, outline=1, fill=1)\n    mask = np.array(img, dtype=np.uint8)\n    masks[img_id] = mask","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"im = Image.fromarray(masks[list(masks.keys())[4]])\nplt.imshow(im)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Runtime data augmentation\ndef blur(img):\n    return (cv2.blur(img,(5,5)))\n\ndef get_train_test_augmented(X_data, Y_data, validation_split=0.25, batch_size=32, seed=seed):\n    X_train, X_test, Y_train, Y_test = train_test_split(X_data,\n                                                        Y_data,\n                                                        train_size=1-validation_split,\n                                                        test_size=validation_split,\n                                                        random_state=seed)\n    \n    # Image data generator distortion options\n    data_gen_args = dict(rotation_range=45.,\n                         width_shift_range=0.1,\n                         height_shift_range=0.1,\n                         shear_range=0.2,\n                         zoom_range=0.2,\n                         horizontal_flip=True,\n                         vertical_flip=True,\n                        # preprocessing_function= blur,\n                         fill_mode='reflect')  #use 'constant'??\n\n\n    # Train data, provide the same seed and keyword arguments to the fit and flow methods\n    X_datagen = ImageDataGenerator(**data_gen_args)\n    Y_datagen = ImageDataGenerator(**data_gen_args)\n    X_datagen.fit(X_train, augment=True, seed=seed)\n    Y_datagen.fit(Y_train, augment=True, seed=seed)\n    X_train_augmented = X_datagen.flow(X_train, batch_size=batch_size, shuffle=True, seed=seed)\n    Y_train_augmented = Y_datagen.flow(Y_train, batch_size=batch_size, shuffle=True, seed=seed)\n      # Test data, no data augmentation, but we create a generator anyway\n    X_datagen_val = ImageDataGenerator()\n    Y_datagen_val = ImageDataGenerator()\n    X_datagen_val.fit(X_test, augment=True, seed=seed)\n    Y_datagen_val.fit(Y_test, augment=True, seed=seed)\n    X_test_augmented = X_datagen_val.flow(X_test, batch_size=batch_size, shuffle=True, seed=seed)\n    Y_test_augmented = Y_datagen_val.flow(Y_test, batch_size=batch_size, shuffle=True, seed=seed)\n    \n    \n    # combine generators into one which yields image and masks\n    train_generator = zip(X_train_augmented, Y_train_augmented)\n    test_generator = zip(X_test_augmented, Y_test_augmented)\n    \n    return train_generator, test_generator\n\n\n","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def change_contrast(img, factor):\n    factor = float(factor)\n    return np.clip(128 + factor * img - factor * 128, 0, 255).astype(np.uint8)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"#from PIL import Image, ImageEnhance\nfactor =2.0\n# Get and resize train images and masks\nX_train = np.zeros((len(train_ids), IMG_HEIGHT, IMG_WIDTH, IMG_CHANNELS), dtype=np.uint8)\nY_train = np.zeros((len(train_ids), IMG_HEIGHT, IMG_WIDTH, 1), dtype=np.bool)\nprint('Getting and resizing train images and masks... ')\nsys.stdout.flush()\n\nfor n, id_ in tqdm(enumerate(train_ids[:]), total=len(train_ids)):\n    path = TRAIN_PATH + id_\n    img = imread(path)[:,:,:IMG_CHANNELS]\n    img = resize(img, (IMG_HEIGHT, IMG_WIDTH), mode='constant', preserve_range=True)\n    #img = change_contrast(img, factor)\n    X_train[n] = img\n    mask = np.zeros((IMG_HEIGHT, IMG_WIDTH, 1), dtype=np.bool)\n    \n    id_clean = id_.split('.')[0]\n    if id_clean in masks.keys():\n        Y_train[n] = masks[id_clean][:, :, np.newaxis]\n        \n\n# Get and resize test images\nX_test = np.zeros((len(test_ids), IMG_HEIGHT, IMG_WIDTH, IMG_CHANNELS), dtype=np.uint8)\nsizes_test = list()\nprint('Getting and resizing test images... ')\nsys.stdout.flush()\nfor n, id_ in tqdm(enumerate(test_ids), total=len(test_ids)):\n    path = TEST_PATH + id_\n    img = imread(path)[:,:,:IMG_CHANNELS]\n    sizes_test.append([img.shape[0], img.shape[1]])\n    img = resize(img, (IMG_HEIGHT, IMG_WIDTH), mode='constant', preserve_range=True)\n    #img = change_contrast(img, factor)\n    X_test[n] = img\n","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"batch_size = 32\n\ntrain_generator, test_generator = get_train_test_augmented(X_train, Y_train, validation_split=0.1, batch_size=batch_size)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"X_train.shape, Y_train.shape","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def show_images(images, num=2):\n    \n    images_to_show = np.random.choice(images, num)\n\n    for image_id in images_to_show:\n\n        image_path = os.path.join(train_folder, image_id + \".jpg\")\n        image = Image.open(image_path)\n\n        # get all bboxes for given image in [xmin, ymin, width, height]\n        bboxes = [literal_eval(box) for box in df[df['image_id'] == image_id]['bbox']]\n\n        # visualize them\n        draw = ImageDraw.Draw(image)\n        for bbox in bboxes:    \n            draw.rectangle([bbox[0], bbox[1], bbox[0] + bbox[2], bbox[1] + bbox[3]], width=3)\n \n        plt.figure(figsize = (15,15))\n        plt.imshow(image)\n        plt.show()\n\n\nunique_images = df['image_id'].unique()\n#print(unique_images)\nshow_images(unique_images)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"#Credits to : https://www.kaggle.com/c/tgs-salt-identification-challenge/discussion/63044\n\ndef castF(x):\n    return K.cast(x, K.floatx())\n\ndef castB(x):\n    return K.cast(x, bool)\n\ndef iou_loss_core(true,pred):  #this can be used as a loss if you make it negative\n    intersection = true * pred\n    notTrue = 1 - true\n    union = true + (notTrue * pred)\n\n    return (K.sum(intersection, axis=-1) + K.epsilon()) / (K.sum(union, axis=-1) + K.epsilon())\n\ndef competitionMetric2(true, pred): #any shape can go - can't be a loss function\n\n    tresholds = [0.5 + (i * 0.05)  for i in range(5)]\n\n    #flattened images (batch, pixels)\n    true = K.batch_flatten(true)\n    pred = K.batch_flatten(pred)\n    pred = castF(K.greater(pred, 0.5))\n\n    #total white pixels - (batch,)\n    trueSum = K.sum(true, axis=-1)\n    predSum = K.sum(pred, axis=-1)\n\n    #has mask or not per image - (batch,)\n    true1 = castF(K.greater(trueSum, 1))    \n    pred1 = castF(K.greater(predSum, 1))\n\n    #to get images that have mask in both true and pred\n    truePositiveMask = castB(true1 * pred1)\n\n    #separating only the possible true positives to check iou\n    testTrue = tf.boolean_mask(true, truePositiveMask)\n    testPred = tf.boolean_mask(pred, truePositiveMask)\n\n    #getting iou and threshold comparisons\n    iou = iou_loss_core(testTrue,testPred) \n    truePositives = [castF(K.greater(iou, tres)) for tres in tresholds]\n\n    #mean of thressholds for true positives and total sum\n    truePositives = K.mean(K.stack(truePositives, axis=-1), axis=-1)\n    truePositives = K.sum(truePositives)\n\n    #to get images that don't have mask in both true and pred\n    trueNegatives = (1-true1) * (1 - pred1) # = 1 -true1 - pred1 + true1*pred1\n    trueNegatives = K.sum(trueNegatives) \n\n    return (truePositives + trueNegatives) / castF(K.shape(true)[0])\n# Custom loss function\ndef dice_coef(y_true, y_pred):\n    smooth = 1.\n    y_true_f = K.flatten(y_true)\n    y_pred_f = K.flatten(y_pred)\n    intersection = K.sum(y_true_f * y_pred_f)\n    return (2. * intersection + smooth) / (K.sum(y_true_f) + K.sum(y_pred_f) + smooth)\n\ndef bce_dice_loss(y_true, y_pred):\n    return 0.5 * keras.losses.binary_crossentropy(y_true, y_pred) - dice_coef(y_true, y_pred)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def conv2d_block(input_tensor, n_filters, kernel_size=3, batchnorm=True):\n    # first layer\n    x = Conv2D(filters=n_filters, kernel_size=(kernel_size, kernel_size), kernel_initializer=\"he_normal\",\n               padding=\"same\")(input_tensor)\n    if batchnorm:\n        x = BatchNormalization()(x)\n    x = Activation(\"relu\")(x)\n    # second layer\n    x = Conv2D(filters=n_filters, kernel_size=(kernel_size, kernel_size), kernel_initializer=\"he_normal\",\n               padding=\"same\")(x)\n    if batchnorm:\n        x = BatchNormalization()(x)\n    x = Activation(\"relu\")(x)\n    return x","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"n_filters=16\ndropout=0.3\nbatchnorm=True\n# Build U-Net model\ninputs = Input((IMG_HEIGHT, IMG_WIDTH, IMG_CHANNELS))\nc1 = conv2d_block(inputs, n_filters=n_filters*1, kernel_size=3, batchnorm=batchnorm)\np1 = MaxPooling2D((2, 2)) (c1)\np1 = Dropout(dropout)(p1)\n\nc2 = conv2d_block(p1, n_filters=n_filters*2, kernel_size=3, batchnorm=batchnorm)\np2 = MaxPooling2D((2, 2)) (c2)\np2 = Dropout(dropout)(p2)\n\nc3 = conv2d_block(p2, n_filters=n_filters*4, kernel_size=3, batchnorm=batchnorm)\np3 = MaxPooling2D((2, 2)) (c3)\np3 = Dropout(dropout)(p3)\n\nc4 = conv2d_block(p3, n_filters=n_filters*8, kernel_size=3, batchnorm=batchnorm)\np4 = MaxPooling2D(pool_size=(2, 2)) (c4)\np4 = Dropout(dropout)(p4)\n    \nc5 = conv2d_block(p4, n_filters=n_filters*16, kernel_size=3, batchnorm=batchnorm)\n    \n    # expansive path\nu6 = Conv2DTranspose(n_filters*8, (3, 3), strides=(2, 2), padding='same') (c5)\nu6 = concatenate([u6, c4])\nu6 = Dropout(dropout)(u6)\nc6 = conv2d_block(u6, n_filters=n_filters*8, kernel_size=3, batchnorm=batchnorm)\n\nu7 = Conv2DTranspose(n_filters*4, (3, 3), strides=(2, 2), padding='same') (c6)\nu7 = concatenate([u7, c3])\nu7 = Dropout(dropout)(u7)\nc7 = conv2d_block(u7, n_filters=n_filters*4, kernel_size=3, batchnorm=batchnorm)\n\nu8 = Conv2DTranspose(n_filters*2, (3, 3), strides=(2, 2), padding='same') (c7)\nu8 = concatenate([u8, c2])\nu8 = Dropout(dropout)(u8)\nc8 = conv2d_block(u8, n_filters=n_filters*2, kernel_size=3, batchnorm=batchnorm)\n\nu9 = Conv2DTranspose(n_filters*1, (3, 3), strides=(2, 2), padding='same') (c8)\nu9 = concatenate([u9, c1], axis=3)\nu9 = Dropout(dropout)(u9)\nc9 = conv2d_block(u9, n_filters=n_filters*1, kernel_size=3, batchnorm=batchnorm)\n    \noutputs = Conv2D(1, (1, 1), activation='sigmoid') (c9)\n\nmodel = Model(inputs=[inputs], outputs=[outputs])\nmodel.compile(optimizer='adam', loss='binary_crossentropy', metrics=[competitionMetric2])\nmodel.summary()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"from keras.utils import plot_model\nplot_model(model, show_shapes=True)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Fit model\nearlystop = EarlyStopping(patience=10, verbose=1, restore_best_weights=True)\n##model.fit(X_train, \n   #      Y_train,\nmodel.fit_generator(\n        train_generator, validation_data=test_generator, \n        validation_steps=batch_size/2, \n        steps_per_epoch= 1000, #len(X_train)/(batch_size*2), \n        epochs=25,\n        callbacks=[earlystop]\n        )\nmodel.save(PATH)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"THRESH=0.6\npreds = model.predict(X_test)[:, :, :, 0]\n\nmasked_preds = preds > THRESH\nprint(len(preds[masked_preds]))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def show_detected_objects(image_name, boxes, scores, labels):\n    img_path = test_img+'/'+image_name\n  \n    image = read_image_bgr(img_path)\n    draw = image.copy()\n    draw = cv2.cvtColor(draw, cv2.COLOR_BGR2RGB)\n\n    draw_detections(draw, boxes, scores, labels)\n    plt.figure(figsize=(15,10))\n    plt.axis('off')\n    plt.imshow(draw)\n    plt.show()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"n_rows = 3\n\nf, ax = plt.subplots(n_rows, 3, figsize=(14, 10))\nbbox_dict = {}\nfor j, idx in enumerate([4,5,6]):\n    for k, kind in enumerate(['original', 'pred', 'masked_pred']):\n        bboxes=list()\n        if kind == 'original':\n            img = X_test[idx]\n                  \n        elif kind == 'pred':\n            img = preds[idx]\n            \n        elif kind == 'masked_pred':\n            masked_pred = preds[idx] > THRESH\n            img = masked_pred\n           \n        ax[j, k].imshow(img)\n\n\nplt.tight_layout()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def get_params_from_bbox(coords, score, scaling_factor=1):\n    xmin, ymin = coords[1] * scaling_factor, coords[0] * scaling_factor\n    w = (coords[3] - coords[1]) * scaling_factor\n    h = (coords[2] - coords[0]) * scaling_factor\n    return score, xmin, ymin, w, h ","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Allows to extract bounding boxes from binary masks\nbboxes = list()\n\nfor j in range(masked_preds.shape[0]):\n    label_j = label(masked_preds[j, :, :])  \n    props = regionprops(label_j)   # that's were the job is done\n    bboxes.append(props)\n    #for b in bboxes[j]:\n        #print(list(b))\n        #print(preds[j, b.coords].max())","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Here we format the bboxes into the required format\nsample_sub = pd.DataFrame(columns=['image_id', 'PredictionString'])\nfor i in range(masked_preds.shape[0]):\n    bboxes_processed = [get_params_from_bbox(bb.bbox, preds[i, bb.coords].max(), scaling_factor=SC_FACTOR) for bb in bboxes[i]]\n    formated_boxes = [' '.join(map(str, bb_m)) for bb_m in bboxes_processed]\n    print([test_ids[i][:-4], \" \".join(formated_boxes)])\n    sample_sub.loc[i]=[test_ids[i][:-4], \" \".join(formated_boxes)]\n\n#print(sample_sub)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"sample_sub","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"sample_sub.to_csv('submission.csv', index=False)","execution_count":null,"outputs":[]}],"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}