{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Approach 3 - Local Features\n",
    "For this approach, we extract local features from the Lapse images using SIFT and cluster them into a vocabulary (Bag of Words approach). Using this vocabulary, we can map every Lapse and Motion image to a BoW feature vector. The Lapse feature vectors make up the training data for a one-class SVM. The decision boundary of the SVM can then be customized to achieve the right compromise between correct keeps and correct eliminations.\n",
    "As we are using SIFT descriptors, we work on grayscale images.\n",
    "\n",
    "## Advantages\n",
    "- scale, rotation and position invariance = invariance to camera movements\n",
    "- not sensitive to lighting changes\n",
    "\n",
    "\n",
    "## Disadvantages\n",
    "- no color information\n",
    "- Instead of detecting the anomalies (animals), we detect the absence of a sufficiently large portion of the background. Smaller objects are therefore especially hard to detect."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Found 3 sessions\n",
      "Session 'Beaver_01' at folder: /home/kleinsteuber/vscode/ResizedSessions_NoBackup/VIELAAS_Spring_Session01-VIELAAS_Beaver_01\n",
      "Loaded scans.\n"
     ]
    }
   ],
   "source": [
    "%load_ext autoreload\n",
    "%autoreload 2\n",
    "\n",
    "from tqdm.notebook import tqdm\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import cv2 as cv\n",
    "\n",
    "from py.PlotUtils import plot_roc_curve, get_percentiles\n",
    "from py.Dataset import Dataset\n",
    "from py.ImageUtils import display_images\n",
    "\n",
    "DIR = '/home/kleinsteuber/vscode/ResizedSessions_NoBackup' # dataset directory\n",
    "\n",
    "ds = Dataset(DIR)\n",
    "session = ds.create_session(\"beaver_01\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Compute SIFT descriptors\n",
    "Since there are often thousands of SIFT keypoints, we limit the number of keypoints to 400. To select larger keypoints (i.e. in higher octaves), we choose a larger sigma of 2.4 (default: 1.6)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 603660 characters]",
      "text/plain": [
       "<Figure size 1152x360 with 2 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Get motion image and closest lapse image for comparison\n",
    "motion_img = session.get_random_motion_image(day_only=True)\n",
    "before_img, after_img, rel = motion_img.get_closest_lapse_images()\n",
    "closest_img = before_img if rel <= 0.5 else after_img\n",
    "\n",
    "# read and display images\n",
    "img = motion_img.read_opencv(gray=True)\n",
    "comp_img = closest_img.read_opencv(gray=True)\n",
    "display_images([img, comp_img], [f\"Input motion img {img.shape}\", f\"Closest lapse img {comp_img.shape}\"], cmap=\"gray\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 799032 characters]",
      "text/plain": [
       "<Figure size 1152x360 with 2 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "def dense_keypoints(img, step=50, size=50):\n",
    "    # calculate offset to center keypoints\n",
    "    off = ((img.shape[0] % step) // 2, (img.shape[1] % step) // 2)\n",
    "    border_dist = (step + 1) // 2\n",
    "    return [cv.KeyPoint(x, y, size) for y in range(border_dist + off[0], img.shape[0] - border_dist, step) \n",
    "                                    for x in range(border_dist + off[1], img.shape[1] - border_dist, step)]\n",
    "\n",
    "sift = cv.SIFT_create()\n",
    "\n",
    "img_kp = dense_keypoints(img)\n",
    "img_kp, img_des = sift.compute(img, img_kp)\n",
    "\n",
    "comp_img_kp = dense_keypoints(comp_img)\n",
    "comp_img_kp, comp_img_des = sift.compute(comp_img, comp_img_kp)\n",
    "\n",
    "kp_img = cv.drawKeypoints(img, img_kp, img, (100, 255, 100), flags=cv.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)\n",
    "comp_kp_img = cv.drawKeypoints(comp_img, comp_img_kp, comp_img, (100, 255, 100), flags=cv.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)\n",
    "\n",
    "display_images([kp_img, comp_kp_img], [f\"Input motion img ({len(img_kp)} keypoints)\", f\"Closest lapse img ({len(comp_img_kp)} keypoints)\"])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 183424 characters]",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 183016 characters]",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Save raw images\n",
    "plt.figure(figsize=(6,4))\n",
    "plt.imshow(kp_img)\n",
    "plt.savefig(\"approach3_keypoints.pdf\", bbox_inches=\"tight\")\n",
    "plt.show()\n",
    "\n",
    "plt.figure(figsize=(6,4))\n",
    "plt.imshow(comp_kp_img)\n",
    "plt.savefig(\"approach3_keypoints_lapse.pdf\", bbox_inches=\"tight\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Matching keypoints\n",
    "We match SIFT descriptors against each other using a BF (brute-force) matcher. In the following, we match the keypoints of the lapse and motion image for testing purposes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 623112 characters]",
      "text/plain": [
       "<Figure size 2304x360 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "bf = cv.BFMatcher()\n",
    "# Match descriptors, calculate best two matches\n",
    "matches = bf.knnMatch(img_des, comp_img_des, k=2)\n",
    "\n",
    "# Apply ratio test\n",
    "good = []\n",
    "for m,n in matches:\n",
    "    if m.distance < 0.75*n.distance:\n",
    "        good.append([m])\n",
    "\n",
    "# cv.drawMatchesKnn expects list of lists as matches.\n",
    "matches_img = cv.drawMatchesKnn(img, img_kp, comp_img, comp_img_kp, good, None, flags=cv.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)\n",
    "display_images([matches_img], [\"Matches\"], size=(16,5))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Evaluation"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The test data must be labeled (normal/anomalous)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "695 test results with 695 labels\n"
     ]
    }
   ],
   "source": [
    "# test labels\n",
    "from py.Labels import LABELS\n",
    "\n",
    "clusters = 1024 # BOW vocabulary size\n",
    "step = 30 # DSIFT keypoint step size\n",
    "size = 30 # DSIFT keypoint size\n",
    "suffix = \"_random\" # \"_motion\" if motion keypoints were included in clustering, \"_random\" if random clustering was used instead of kmeans\n",
    "test_labels = []\n",
    "test_df = []\n",
    "with open(f\"./bow_train_NoBackup/{session.name}/bow_eval_{step}_{size}_{clusters}{suffix}.csv\", \"r\") as f:\n",
    "    for line in f:\n",
    "        entries = line.split(\",\")\n",
    "        filename = entries[0]\n",
    "        img_number = int(filename[-9:-4])\n",
    "        if img_number > LABELS[session.name][\"max\"] or img_number in LABELS[session.name][\"not_annotated\"]:\n",
    "            continue\n",
    "        is_normal = (img_number in LABELS[session.name][\"normal\"])\n",
    "        test_labels.append(1 if is_normal else -1)\n",
    "        test_df.append([float(df) for df in entries[1:]])\n",
    "test_labels = np.array(test_labels)\n",
    "test_df = np.array(test_df)\n",
    "print(f\"{len(test_df)} test results with {len(test_labels)} labels\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.106 labeled normal, 0.894 anomalous\n",
      "Accuracy:                               0.812\n",
      "True anomalous rate (must be high!):    0.459 \t - How many animals are detected as animals? (recall)\n",
      "True normal rate (higher is better):    0.853 \t - How many empties are detected as empties?\n",
      "False anomalous rate (lower is better): 0.147 \t - How many empties are detected as animals?\n",
      "False normal rate (must be low!):       0.541 \t - How many animals are detected as empties?\n",
      "Precision (higher is better):           0.272 \t - How many detected animals are actually animals?\n"
     ]
    }
   ],
   "source": [
    "def eval_test(test_labels, test_df, df_threshold = 0):\n",
    "    test_results = np.where(test_df >= df_threshold, np.ones(test_df.shape[0]), -np.ones(test_df.shape[0]))\n",
    "\n",
    "    accuracy = np.mean(test_results == test_labels)\n",
    "    tar = np.sum((test_results == 1) & (test_labels == 1)) / np.sum(test_labels == 1)\n",
    "    tnr = np.sum((test_results == -1) & (test_labels == -1)) / np.sum(test_labels == -1)\n",
    "    far = np.sum((test_results == 1) & (test_labels == -1)) / np.sum(test_labels == -1)\n",
    "    fnr = np.sum((test_results == -1) & (test_labels == 1)) / np.sum(test_labels == 1)\n",
    "    precision = (np.sum((test_results == 1) & (test_labels == 1)) / np.sum(test_results == 1)) if np.any(test_results == 1) else 0\n",
    "\n",
    "    return accuracy, tar, tnr, far, fnr, precision\n",
    "\n",
    "accuracy, tar, tnr, far, fnr, precision = eval_test(test_labels, test_df[:,0])\n",
    "\n",
    "labeled_normal = np.mean(test_labels == 1)\n",
    "print(f\"{labeled_normal:.3f} labeled normal, {(1 - labeled_normal):.3f} anomalous\")\n",
    "\n",
    "print(f\"Accuracy:                               {accuracy:.3f}\")\n",
    "print(f\"True anomalous rate (must be high!):    {tar:.3f} \\t - How many animals are detected as animals? (recall)\")\n",
    "print(f\"True normal rate (higher is better):    {tnr:.3f} \\t - How many empties are detected as empties?\")\n",
    "print(f\"False anomalous rate (lower is better): {far:.3f} \\t - How many empties are detected as animals?\")\n",
    "print(f\"False normal rate (must be low!):       {fnr:.3f} \\t - How many animals are detected as empties?\")\n",
    "print(f\"Precision (higher is better):           {precision:.3f} \\t - How many detected animals are actually animals?\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 22336 characters]",
      "text/plain": [
       "<Figure size 576x576 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.9 percentile : TPR = 0.9054, FPR = 0.4251 <-> TNR = 0.5749 @ thresh -57.98641920691854\n",
      "0.95 percentile : TPR = 0.9595, FPR = 0.4461 <-> TNR = 0.5539 @ thresh -61.21968592548046\n",
      "0.98 percentile : TPR = 1.0000, FPR = 0.7246 <-> TNR = 0.2754 @ thresh -86.32043784641692\n",
      "0.99 percentile : TPR = 1.0000, FPR = 0.7246 <-> TNR = 0.2754 @ thresh -86.32043784641692\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 22240 characters]",
      "text/plain": [
       "<Figure size 576x576 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.9 percentile : TPR = 0.9324, FPR = 0.4783 <-> TNR = 0.5217 @ thresh -57.96909824319087\n",
      "0.95 percentile : TPR = 0.9595, FPR = 0.5330 <-> TNR = 0.4670 @ thresh -62.89029834951606\n",
      "0.98 percentile : TPR = 0.9865, FPR = 0.8406 <-> TNR = 0.1594 @ thresh -98.31221120460783\n",
      "0.99 percentile : TPR = 1.0000, FPR = 0.8486 <-> TNR = 0.1514 @ thresh -99.81392414777008\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 22096 characters]",
      "text/plain": [
       "<Figure size 576x576 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.9 percentile : TPR = 0.9054, FPR = 0.4203 <-> TNR = 0.5797 @ thresh -46.42222143397851\n",
      "0.95 percentile : TPR = 0.9595, FPR = 0.4831 <-> TNR = 0.5169 @ thresh -57.77152350231944\n",
      "0.98 percentile : TPR = 0.9865, FPR = 0.8792 <-> TNR = 0.1208 @ thresh -115.37954689562615\n",
      "0.99 percentile : TPR = 1.0000, FPR = 0.8889 <-> TNR = 0.1111 @ thresh -116.92268156510316\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 22192 characters]",
      "text/plain": [
       "<Figure size 576x576 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.9 percentile : TPR = 0.9054, FPR = 0.5040 <-> TNR = 0.4960 @ thresh -61.930335188957514\n",
      "0.95 percentile : TPR = 0.9595, FPR = 0.5862 <-> TNR = 0.4138 @ thresh -69.43002627253318\n",
      "0.98 percentile : TPR = 0.9865, FPR = 0.8567 <-> TNR = 0.1433 @ thresh -105.25911261294101\n",
      "0.99 percentile : TPR = 1.0000, FPR = 0.8631 <-> TNR = 0.1369 @ thresh -106.31607699933667\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 22144 characters]",
      "text/plain": [
       "<Figure size 576x576 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.9 percentile : TPR = 0.9054, FPR = 0.4267 <-> TNR = 0.5733 @ thresh -53.909889538817794\n",
      "0.95 percentile : TPR = 0.9865, FPR = 0.7504 <-> TNR = 0.2496 @ thresh -90.7365004449735\n",
      "0.98 percentile : TPR = 0.9865, FPR = 0.7504 <-> TNR = 0.2496 @ thresh -90.7365004449735\n",
      "0.99 percentile : TPR = 1.0000, FPR = 0.7633 <-> TNR = 0.2367 @ thresh -93.59593916928242\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 22492 characters]",
      "text/plain": [
       "<Figure size 576x576 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.9 percentile : TPR = 0.9054, FPR = 0.4074 <-> TNR = 0.5926 @ thresh -50.9529406696179\n",
      "0.95 percentile : TPR = 0.9595, FPR = 0.6329 <-> TNR = 0.3671 @ thresh -70.21557341362325\n",
      "0.98 percentile : TPR = 0.9865, FPR = 0.9581 <-> TNR = 0.0419 @ thresh -134.91997546198\n",
      "0.99 percentile : TPR = 1.0000, FPR = 0.9597 <-> TNR = 0.0403 @ thresh -135.2997479443061\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 22444 characters]",
      "text/plain": [
       "<Figure size 576x576 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.9 percentile : TPR = 0.9054, FPR = 0.4412 <-> TNR = 0.5588 @ thresh -59.7249890637227\n",
      "0.95 percentile : TPR = 0.9595, FPR = 0.8325 <-> TNR = 0.1675 @ thresh -104.88935442840156\n",
      "0.98 percentile : TPR = 0.9865, FPR = 0.9308 <-> TNR = 0.0692 @ thresh -125.43362546843687\n",
      "0.99 percentile : TPR = 1.0000, FPR = 0.9404 <-> TNR = 0.0596 @ thresh -127.38773813743325\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 22248 characters]",
      "text/plain": [
       "<Figure size 576x576 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.9 percentile : TPR = 0.9054, FPR = 0.4058 <-> TNR = 0.5942 @ thresh -46.53070784749596\n",
      "0.95 percentile : TPR = 0.9595, FPR = 0.5362 <-> TNR = 0.4638 @ thresh -64.09642838226235\n",
      "0.98 percentile : TPR = 0.9865, FPR = 0.9823 <-> TNR = 0.0177 @ thresh -141.45833482134157\n",
      "0.99 percentile : TPR = 1.0000, FPR = 0.9839 <-> TNR = 0.0161 @ thresh -143.72105353295728\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 22328 characters]",
      "text/plain": [
       "<Figure size 576x576 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.9 percentile : TPR = 0.9054, FPR = 0.4992 <-> TNR = 0.5008 @ thresh -55.06037151525953\n",
      "0.95 percentile : TPR = 0.9595, FPR = 0.7021 <-> TNR = 0.2979 @ thresh -69.55509817226138\n",
      "0.98 percentile : TPR = 1.0000, FPR = 0.8728 <-> TNR = 0.1272 @ thresh -97.60153074288088\n",
      "0.99 percentile : TPR = 1.0000, FPR = 0.8728 <-> TNR = 0.1272 @ thresh -97.60153074288088\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 22384 characters]",
      "text/plain": [
       "<Figure size 576x576 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.9 percentile : TPR = 0.9054, FPR = 0.3720 <-> TNR = 0.6280 @ thresh -49.69415716064934\n",
      "0.95 percentile : TPR = 0.9595, FPR = 0.4251 <-> TNR = 0.5749 @ thresh -59.1537640092654\n",
      "0.98 percentile : TPR = 0.9865, FPR = 0.9469 <-> TNR = 0.0531 @ thresh -135.67460585387744\n",
      "0.99 percentile : TPR = 1.0000, FPR = 0.9501 <-> TNR = 0.0499 @ thresh -135.8753471614021\n",
      "AUC avg = 0.7758 2*std = 0.0163 min = 0.7658 max = 0.7946\n",
      "TNR for TPR >= .9  avg = 0.5620 2*std = 0.0817 min = 0.4960 max = 0.6280\n",
      "TNR for TPR >= .95 avg = 0.4072 2*std = 0.2564 min = 0.1675 max = 0.5749\n",
      "TNR for TPR >= .98 avg = 0.1258 2*std = 0.1629 min = 0.0177 max = 0.2754\n",
      "TNR for TPR >= .99 avg = 0.1205 2*std = 0.1611 min = 0.0161 max = 0.2754\n"
     ]
    }
   ],
   "source": [
    "aucs = []\n",
    "percentiles = []\n",
    "for i in range(test_df.shape[1]):\n",
    "    fpr, tpr, thresholds, auc_score = plot_roc_curve(test_labels, test_df[:,i], f\"BOW{suffix} k={clusters}, step={step}, size={size}\")\n",
    "    #savefile=f\"plots/approach3/roc_curves/{session.name}_{step}_{size}_{clusters}{suffix}\"\n",
    "    tnrs = get_percentiles(fpr, tpr, thresholds)\n",
    "    aucs.append(auc_score)\n",
    "    percentiles.append(tnrs)\n",
    "percentiles = np.array(percentiles)\n",
    "print(f\"AUC avg = {np.mean(aucs):.4f} 2*std = {2*np.std(aucs):.4f} min = {np.min(aucs):.4f} max = {np.max(aucs):.4f}\")\n",
    "for i, tpr in enumerate([\".9 \", \".95\", \".98\", \".99\"]):\n",
    "    print(f\"TNR for TPR >= {tpr} avg = {np.mean(percentiles[:,i]):.4f} 2*std = {2*np.std(percentiles[:,i]):.4f} min = {np.min(percentiles[:,i]):.4f} max = {np.max(percentiles[:,i]):.4f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## AUC Scores\n",
    "\n",
    "### Beaver_01\n",
    "\n",
    "| $k$ (clusters) | $kp$ (keypoint step size) | AUC |\n",
    "| --- | --- | ---: |\n",
    "| 512 | 30 | 0.7538 |\n",
    "| 1024 | 30 | 0.7698 |\n",
    "| 2048 | 30 | 0.7741 |\n",
    "| 1024 | 20 | 0.7406 |\n",
    "| 1024 | 30, size 60 | 0.7383 |\n",
    "\n",
    "### Marten_01\n",
    "\n",
    "| $k$ (clusters) | $kp$ (keypoint step size) | AUC |\n",
    "| --- | --- | ---: |\n",
    "| 512 | 30 | 0.7079 |\n",
    "| 1024 | 30 | 0.7099 |\n",
    "| 2048 | 30 | 0.6557 |"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3.6.9 64-bit",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.9"
  },
  "orig_nbformat": 4,
  "vscode": {
   "interpreter": {
    "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6"
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
