{"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":"code","source":"!pip install mediapy","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","execution":{"iopub.status.busy":"2023-04-11T20:30:28.956953Z","iopub.execute_input":"2023-04-11T20:30:28.957233Z","iopub.status.idle":"2023-04-11T20:30:41.778665Z","shell.execute_reply.started":"2023-04-11T20:30:28.957204Z","shell.execute_reply":"2023-04-11T20:30:41.777482Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Get some utility functions from https://github.com/cvg/Hierarchical-Localization/\n\n%cd /kaggle/working/\n!rm -rf /kaggle/working/Hierarchical-Localization\n!git clone --quiet --recursive https://github.com/cvg/Hierarchical-Localization/\n%cd /kaggle/working/Hierarchical-Localization\n!pip install -e .\n\nfrom hloc import extract_features, match_features, reconstruction, visualization, pairs_from_exhaustive\nfrom hloc.visualization import plot_images, read_image\nfrom hloc.utils import viz_3d\n\n%cd /kaggle/working/","metadata":{"execution":{"iopub.status.busy":"2023-04-11T20:30:48.139625Z","iopub.execute_input":"2023-04-11T20:30:48.139991Z","iopub.status.idle":"2023-04-11T20:31:17.614820Z","shell.execute_reply.started":"2023-04-11T20:30:48.139957Z","shell.execute_reply":"2023-04-11T20:31:17.613684Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import os\nimport pycolmap\nimport numpy as np\nimport mediapy as media\nimport cv2\nfrom glob import glob\nfrom pathlib import Path\nfrom time import time","metadata":{"execution":{"iopub.status.busy":"2023-04-11T20:31:22.043224Z","iopub.execute_input":"2023-04-11T20:31:22.043875Z","iopub.status.idle":"2023-04-11T20:31:22.074965Z","shell.execute_reply.started":"2023-04-11T20:31:22.043835Z","shell.execute_reply":"2023-04-11T20:31:22.073647Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"dataset = 'urban'\nscene = 'kyiv-puppet-theater'\nsrc = f'/kaggle/input/image-matching-challenge-2023/train/{dataset}/{scene}'\n\nimages = [cv2.cvtColor(cv2.imread(im), cv2.COLOR_BGR2RGB) for im in glob(f'{src}/images/*')]","metadata":{"execution":{"iopub.status.busy":"2023-04-11T20:31:23.478814Z","iopub.execute_input":"2023-04-11T20:31:23.479543Z","iopub.status.idle":"2023-04-11T20:31:24.680226Z","shell.execute_reply.started":"2023-04-11T20:31:23.479499Z","shell.execute_reply":"2023-04-11T20:31:24.679015Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"media.show_images(images, height=300, columns=4)","metadata":{"execution":{"iopub.status.busy":"2023-04-11T20:31:24.682042Z","iopub.execute_input":"2023-04-11T20:31:24.682454Z","iopub.status.idle":"2023-04-11T20:31:26.543029Z","shell.execute_reply.started":"2023-04-11T20:31:24.682415Z","shell.execute_reply":"2023-04-11T20:31:26.542028Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Visualizing a Colmap reconstruction","metadata":{}},{"cell_type":"code","source":"# Visualize a ground-truth 3D reconstruction (points and cameras). Note that some of these might be very large!\n# You can also visualize them outside a notebook with the colmap gui: https://colmap.github.io/\n\nrec_gt = pycolmap.Reconstruction(f'{src}/sfm')\n\nfig = viz_3d.init_figure()\nviz_3d.plot_cameras(fig, rec_gt, color='rgba(50,255,50, 0.5)', name=\"Ground Truth\", size=10)\nviz_3d.plot_reconstruction(fig, rec_gt, cameras = False, color='rgba(255,50,255, 0.5)', name=\"Ground Truth\", cs=5)\nfig.show()","metadata":{"execution":{"iopub.status.busy":"2023-04-11T20:31:32.540845Z","iopub.execute_input":"2023-04-11T20:31:32.541219Z","iopub.status.idle":"2023-04-11T20:31:38.528463Z","shell.execute_reply.started":"2023-04-11T20:31:32.541186Z","shell.execute_reply":"2023-04-11T20:31:38.524567Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Creating a reconstruction with SIFT and Colmap","metadata":{}},{"cell_type":"code","source":"tgt = f'/kaggle/working/{dataset}-{scene}'\n\n# Generate a simple reconstruction with SIFT (https://en.wikipedia.org/wiki/Scale-invariant_feature_transform).\nif not os.path.isdir(tgt):\n    os.makedirs(f'{tgt}/bundle')\n    os.system(f'cp -r {src}/images {tgt}/images')\n\n    database_path = f'{tgt}/database.db'\n\n    sift_opt = pycolmap.SiftExtractionOptions()\n    sift_opt.max_image_size = 1500  # Extract features at low resolution could significantly reduce the overall accuracy\n    sift_opt.max_num_features = 8192  # Generally more features is better, even if behond a certain number it doesn't help incresing accuracy\n    sift_opt.upright = True  # rotation invariance\n    device = 'cpu'\n\n    t = time()\n    pycolmap.extract_features(database_path, f'{tgt}/images', sift_options=sift_opt, verbose=True)\n    print(len(os.listdir(f'{tgt}/images')))\n    print('TIMINGS --- Feature extraction', time() - t)\n\n    t = time()\n    matching_opt = pycolmap.SiftMatchingOptions()\n    matching_opt.max_ratio = 0.85 # Ratio threshold significantly influence the performance of the feature extraction method. It varies depending on the local feature but also on the image type \n#     matching_opt.max_distance = 0.7\n    matching_opt.cross_check = True\n    matching_opt.max_error = 1.0 # The ransac error threshold could help to exclude less accurate tie points\n    pycolmap.match_exhaustive(database_path, sift_options=matching_opt, device=device, verbose=True)\n    print('TIMINGS --- Feature matching', time() - t)\n\n    t = time()\n    mapper_options = pycolmap.IncrementalMapperOptions()\n    mapper_options.extract_colors = False\n    mapper_options.min_model_size = 3\n\n    # Sometimes you want to impose the first image pair for initialize the incremental reconstruction\n    mapper_options.init_image_id1 = -1 \n    mapper_options.init_image_id2 = -1\n\n    # Choose which interior will be refined during BA\n    mapper_options.ba_refine_focal_length = True\n    mapper_options.ba_refine_principal_point = True\n    mapper_options.ba_refine_extra_params = True\n\n    maps = pycolmap.incremental_mapping(database_path=database_path, image_path=f'{tgt}/images', output_path=f'{tgt}/bundle', options=mapper_options)\n    print('TIMINGS --- Mapping', time() - t)","metadata":{"execution":{"iopub.status.busy":"2023-04-11T20:32:30.284153Z","iopub.execute_input":"2023-04-11T20:32:30.284919Z","iopub.status.idle":"2023-04-11T20:42:45.383247Z","shell.execute_reply.started":"2023-04-11T20:32:30.284880Z","shell.execute_reply":"2023-04-11T20:42:45.381995Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"maps","metadata":{"execution":{"iopub.status.busy":"2023-04-11T20:43:19.740961Z","iopub.execute_input":"2023-04-11T20:43:19.741327Z","iopub.status.idle":"2023-04-11T20:43:19.749465Z","shell.execute_reply.started":"2023-04-11T20:43:19.741284Z","shell.execute_reply":"2023-04-11T20:43:19.748467Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Inspect the largest map generated by colmap. Note that it might produce more than one reconstruction.\n# Don't expect great results! SIFT might work well on some scenes, but not others.\n\nbest_index = None\nbest_num_reg_images = 0\nfor idx in maps:\n    if maps[idx].num_reg_images() > best_num_reg_images:\n        best_index = idx\n        best_num_reg_images = maps[idx].num_reg_images()\n\nif best_num_reg_images > 0:\n    print(f'Looking at reconstruction #{best_index} with {best_num_reg_images} registered images')\n\n    fig = viz_3d.init_figure()\n    viz_3d.plot_reconstruction(fig, maps[best_index], color='rgba(0,0,255,0.5)', name=\"Reconstruction\", cs=5, cameras=False)\n    viz_3d.plot_reconstruction(fig, maps[best_index], color='rgba(255,0,0,0.5)', name=\"Reconstruction\", cs=5, points=False)\n    fig.show()\nelse:\n    print('No reconstruction. :(')","metadata":{"execution":{"iopub.status.busy":"2023-04-11T20:45:01.238054Z","iopub.execute_input":"2023-04-11T20:45:01.238429Z","iopub.status.idle":"2023-04-11T20:45:01.333811Z","shell.execute_reply.started":"2023-04-11T20:45:01.238395Z","shell.execute_reply":"2023-04-11T20:45:01.332523Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Writing the \"submission\"","metadata":{}},{"cell_type":"code","source":"# Generate a \"submission\" from colmap.\n# Note that to get a functional submission you'll have to process every scene.\n\ndef arr_to_str(a):\n    return ';'.join([str(x) for x in a.reshape(-1)])\n\n# Get a list of images on this dataset.\nall_images = []\nwith open('/kaggle/input/image-matching-challenge-2023/train/train_labels.csv', 'r') as f:\n    for i, l in enumerate(f):\n        if i > 0 and l:\n            _dataset, _scene, image_path, _, _ = l.strip().split(',')\n            if dataset == _dataset and scene == _scene:\n                all_images.append(image_path)\n\nif best_num_reg_images > 0:\n    rec = maps[best_index]\n    \n    with open('submission.csv', 'w') as f:\n        f.write('image_path,dataset,scene,rotation_matrix,translation_vector\\n')\n        registered_images = []\n        for imgidx, img in rec.images.items():\n            R = img.rotmat()\n            T = img.tvec\n            img_name  = img.name\n            f.write(f'{dataset}/{scene}/images/{img_name},{dataset},{scene},{arr_to_str(R)},{arr_to_str(T)}\\n')\n            registered_images.append(img_name)\n        \n        # Fill out unregistered images with zeros or the score function will fail.\n        for img_name in all_images:\n            if img_name not in registered_images:\n                R = np.zeros(9)\n                T = np.zeros(3)\n                f.write(f'{img_name},{dataset},{scene},{arr_to_str(R)},{arr_to_str(T)}\\n')\n\n    !cat submission.csv","metadata":{"execution":{"iopub.status.busy":"2023-04-11T20:46:02.964085Z","iopub.execute_input":"2023-04-11T20:46:02.964464Z","iopub.status.idle":"2023-04-11T20:46:04.020589Z","shell.execute_reply.started":"2023-04-11T20:46:02.964427Z","shell.execute_reply":"2023-04-11T20:46:04.019400Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Better features than SIFT?\n\nLet's try one of the IMC2020 top-solutions - the DISK local feature extractor.\n\nWe will have to write code to extract features, and then import them into the colmap database. \n\nYou can find examples here: [imc2023-kornia-starter-pack](https://github.com/ducha-aiki/imc2023-kornia-starter-pack.git).","metadata":{}},{"cell_type":"code","source":"%cd /kaggle/working/\n!rm -rf /kaggle/working/imc2023-kornia-starter-pack\n!git clone --quiet --recursive https://github.com/ducha-aiki/imc2023-kornia-starter-pack.git\n\nimport sys\nsys.path.append('/kaggle/working/imc2023-kornia-starter-pack')\n\nimport kornia as K\nimport kornia.feature as KF\nimport torch\nimport h5py\nfrom fastprogress import progress_bar\n","metadata":{"execution":{"iopub.status.busy":"2023-04-11T20:46:07.536795Z","iopub.execute_input":"2023-04-11T20:46:07.537845Z","iopub.status.idle":"2023-04-11T20:46:11.962929Z","shell.execute_reply.started":"2023-04-11T20:46:07.537805Z","shell.execute_reply":"2023-04-11T20:46:11.961701Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# We will need images in pytorch format\ndef load_torch_image(fname, device=torch.device('cpu')):\n    img = K.image_to_tensor(cv2.imread(fname), False).float() / 255.\n    img = K.color.bgr_to_rgb(img.to(device))\n    return img\n\n# Load DISK w/o internet connection through Kaggle Models voodoo: while not necessary yet, it will be helpful for offline submissions, which require turning off internet access.\ndef load_DISK(device=torch.device('cpu')):\n    disk = KF.DISK().to(device)\n    pretrained_dict = torch.load('/kaggle/input/disk/pytorch/depth-supervision/1/loftr_outdoor.ckpt', map_location=device)\n    disk.load_state_dict(pretrained_dict['extractor'])\n    disk.eval()\n    return disk","metadata":{"execution":{"iopub.status.busy":"2023-04-11T20:46:36.208517Z","iopub.execute_input":"2023-04-11T20:46:36.208887Z","iopub.status.idle":"2023-04-11T20:46:36.216511Z","shell.execute_reply.started":"2023-04-11T20:46:36.208854Z","shell.execute_reply":"2023-04-11T20:46:36.215330Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def detect_features(img_fnames,\n                    num_feats = 2048,\n                    device=torch.device('cpu'),\n                    feature_dir = '.featureout'):\n    disk = load_DISK(device)\n    if not os.path.isdir(feature_dir):\n        os.makedirs(feature_dir)\n    \n    # We will save features to h5 files, and then will import them into colmap\n    with h5py.File(f'{feature_dir}/keypoints.h5', mode='w') as f_kp, \\\n         h5py.File(f'{feature_dir}/descriptors.h5', mode='w') as f_desc:\n        for img_path in progress_bar(img_fnames):\n            img_fname = img_path.split('/')[-1]\n            key = img_fname\n            with torch.inference_mode():\n                timg = load_torch_image(img_path, device=device)\n                features = disk(timg, num_feats, pad_if_not_divisible=True)[0]\n                kpts, descs = features.keypoints, features.descriptors\n            \n            # Convert to numpy to store in h5py\n            kpts = kpts.reshape(-1, 2).detach().cpu().numpy()\n            descs = descs.reshape(-1, 128).detach().cpu().numpy()\n            f_kp[key] = kpts\n            f_desc[key] = descs\n    return\n\nimg_fnames =  [fname for fname in glob(f'{src}/images/*')]\nfeature_dir = 'disk_features'\ndevice=torch.device('cuda')\ndetect_features(img_fnames, 5000, device=device, feature_dir=feature_dir)","metadata":{"execution":{"iopub.status.busy":"2023-04-11T20:46:36.777514Z","iopub.execute_input":"2023-04-11T20:46:36.778262Z","iopub.status.idle":"2023-04-11T20:46:51.201606Z","shell.execute_reply.started":"2023-04-11T20:46:36.778226Z","shell.execute_reply":"2023-04-11T20:46:51.200604Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"%%capture\n# kornia_moons for feature visualization\n!pip install kornia_moons","metadata":{"execution":{"iopub.status.busy":"2023-04-11T20:46:54.720260Z","iopub.execute_input":"2023-04-11T20:46:54.720958Z","iopub.status.idle":"2023-04-11T20:47:04.478271Z","shell.execute_reply.started":"2023-04-11T20:46:54.720919Z","shell.execute_reply":"2023-04-11T20:47:04.476734Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Let's visualize our detections\nfrom kornia_moons.feature import visualize_LAF\n\nimage_index = 5\nwith h5py.File(f'{feature_dir}/keypoints.h5', mode='r') as f_kp:\n    img1 = load_torch_image(img_fnames[image_index])\n    key = img_fnames[image_index].split('/')[-1]\n    lafs = KF.laf_from_center_scale_ori(torch.from_numpy(f_kp[key][...]).reshape(1,-1, 2))\n    visualize_LAF(img1, lafs)\n","metadata":{"execution":{"iopub.status.busy":"2023-04-11T20:48:33.588311Z","iopub.execute_input":"2023-04-11T20:48:33.588724Z","iopub.status.idle":"2023-04-11T20:48:38.406840Z","shell.execute_reply.started":"2023-04-11T20:48:33.588688Z","shell.execute_reply":"2023-04-11T20:48:38.405703Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Now we will match our features on GPU with kornia\n\ndef match_features(img_fnames,\n                   index_pairs,\n                   feature_dir = '.featureout',\n                   device=torch.device('cpu'),\n                   min_matches=15, verbose = True):\n    with h5py.File(f'{feature_dir}/descriptors.h5', mode='r') as f_desc, \\\n         h5py.File(f'{feature_dir}/matches.h5', mode='w') as f_match:\n        for pair_idx in progress_bar(index_pairs):\n                    idx1, idx2 = pair_idx\n                    fname1, fname2 = img_fnames[idx1], img_fnames[idx2]\n                    key1, key2 = fname1.split('/')[-1], fname2.split('/')[-1]\n                    desc1 = torch.from_numpy(f_desc[key1][...]).to(device)\n                    desc2 = torch.from_numpy(f_desc[key2][...]).to(device)\n                    # Matching with mutual nearest neighbor check and Lowe's threshold\n                    dists, idxs = KF.match_smnn(desc1, desc2, 0.98)\n                    if len(idxs)  == 0:\n                        continue\n                    n_matches = len(idxs)\n                    if verbose:\n                        print (f'{key1}-{key2}: {n_matches} matches')\n                    group  = f_match.require_group(key1)\n                    if n_matches >= min_matches:\n                         group.create_dataset(key2, data=idxs.detach().cpu().numpy().reshape(-1, 2))\n    return\n\n# matching all to all\nindex_pairs = []\nfor i in range(len(img_fnames)):\n    for j in range(i+1, len(img_fnames)):\n        index_pairs.append((i,j))\n\nmatch_features(img_fnames, index_pairs, device=torch.device('cuda'), feature_dir=feature_dir)","metadata":{"execution":{"iopub.status.busy":"2023-04-11T20:48:40.728232Z","iopub.execute_input":"2023-04-11T20:48:40.729383Z","iopub.status.idle":"2023-04-11T20:49:00.996671Z","shell.execute_reply.started":"2023-04-11T20:48:40.729334Z","shell.execute_reply":"2023-04-11T20:49:00.995543Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Import into colmap\n\nfrom h5_to_db import import_into_colmap\n\ndatabase_path = f'{tgt}/database_disk.db'\n!rm -rf {database_path}\nimg_dir = f'{src}/images'\n\nimport_into_colmap(img_dir, database_path=database_path, feature_dir=feature_dir)","metadata":{"execution":{"iopub.status.busy":"2023-04-11T20:49:03.157324Z","iopub.execute_input":"2023-04-11T20:49:03.158594Z","iopub.status.idle":"2023-04-11T20:49:04.416128Z","shell.execute_reply.started":"2023-04-11T20:49:03.158546Z","shell.execute_reply":"2023-04-11T20:49:04.414849Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Now we run colmap. First matching - because it is bundled with RANSAC.\n# Don't worry, colmap would not match our features again, it will just estimate geometry.\noutput_path =  f'{tgt}/disk_reconstruction'\nos.makedirs(output_path, exist_ok=True)\npycolmap.match_exhaustive(database_path)\n\n# Then we run reconstruction, as before\nmaps = pycolmap.incremental_mapping(database_path, img_dir, output_path)","metadata":{"execution":{"iopub.status.busy":"2023-04-11T20:49:04.754215Z","iopub.execute_input":"2023-04-11T20:49:04.755813Z","iopub.status.idle":"2023-04-11T20:51:08.671642Z","shell.execute_reply.started":"2023-04-11T20:49:04.755757Z","shell.execute_reply":"2023-04-11T20:51:08.670500Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"maps","metadata":{"execution":{"iopub.status.busy":"2023-04-11T20:51:08.673981Z","iopub.execute_input":"2023-04-11T20:51:08.674782Z","iopub.status.idle":"2023-04-11T20:51:08.683390Z","shell.execute_reply.started":"2023-04-11T20:51:08.674740Z","shell.execute_reply":"2023-04-11T20:51:08.682272Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Inspect the largest map generated by DISK + colmap. \n# Note that it might produce more than one reconstruction.\n\nbest_index = None\nbest_num_reg_images = 0\nfor idx in maps:\n    if maps[idx].num_reg_images() > best_num_reg_images:\n        best_index = idx\n        best_num_reg_images = maps[idx].num_reg_images()\n\nif best_num_reg_images > 0:\n    print(f'Looking at reconstruction #{best_index} with {best_num_reg_images} registered images')\n    fig = viz_3d.init_figure()\n    viz_3d.plot_reconstruction(fig, maps[best_index], color='rgba(0,0,255,0.5)', name=\"Reconstruction\", cs=3, cameras=False)\n    viz_3d.plot_reconstruction(fig, maps[best_index], color='rgba(255,0,0,0.5)', name=\"Reconstruction\", cs=3, points=False)\n    fig.show()\nelse:\n    print('No reconstruction. :(')","metadata":{"execution":{"iopub.status.busy":"2023-04-11T20:51:19.089881Z","iopub.execute_input":"2023-04-11T20:51:19.090492Z","iopub.status.idle":"2023-04-11T20:51:19.314433Z","shell.execute_reply.started":"2023-04-11T20:51:19.090440Z","shell.execute_reply":"2023-04-11T20:51:19.313500Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Now we will try to align the obtained reconstruction to the GT one with Helmert transform\ncommon_fnames = []\nfor k, v in maps[best_index].images.items():\n    common_fnames.append(v.name)\n\ngt_positions = []\nfor img_fname in common_fnames:\n    for k,v in rec_gt.images.items():\n        if v.name == img_fname:\n            break\n    gt_positions.append(v.projection_center().reshape(3, 1))\n\nmin_common_images = 10\ntransform = maps[best_index].align_robust(common_fnames, gt_positions, 5)\nprint (transform)","metadata":{"execution":{"iopub.status.busy":"2023-04-11T20:51:35.626346Z","iopub.execute_input":"2023-04-11T20:51:35.627068Z","iopub.status.idle":"2023-04-11T20:51:35.641065Z","shell.execute_reply.started":"2023-04-11T20:51:35.627030Z","shell.execute_reply":"2023-04-11T20:51:35.639949Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Visualize the alignment.\n# Note that the ground truth will show more cameras than we provide you for the reconstruction.\n\nprint(rec_gt)\nprint()\n\nfig = viz_3d.init_figure()\nviz_3d.plot_reconstruction(fig, maps[best_index], color='rgba(0,255,255,0.5)', name=\"Reconstruction\", cs=5)\nviz_3d.plot_reconstruction(fig, rec_gt, points=False, color='rgba(0,100,0,0.1)', name=\"GT Reconstruction\", cs=5)\nfig.show()\n","metadata":{"execution":{"iopub.status.busy":"2023-04-11T20:53:44.192217Z","iopub.execute_input":"2023-04-11T20:53:44.192840Z","iopub.status.idle":"2023-04-11T20:53:46.414254Z","shell.execute_reply.started":"2023-04-11T20:53:44.192799Z","shell.execute_reply":"2023-04-11T20:53:46.413366Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]}]}