{"metadata":{"kernelspec":{"display_name":"Python 3","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.11.11"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":49349,"databundleVersionId":5447706,"isSourceIdPinned":false,"sourceType":"competition"}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false},"papermill":{"default_parameters":{},"duration":744.584316,"end_time":"2025-07-02T04:29:28.568570","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-07-02T04:17:03.984254","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# IMC2023 Fountain: PyCOLMAP Camera Positions","metadata":{"papermill":{"duration":0.004661,"end_time":"2025-07-02T04:17:08.739702","exception":false,"start_time":"2025-07-02T04:17:08.735041","status":"completed"},"tags":[]}},{"cell_type":"code","source":"!pip install pycolmap","metadata":{"_kg_hide-output":true,"execution":{"iopub.execute_input":"2025-07-02T04:17:08.749068Z","iopub.status.busy":"2025-07-02T04:17:08.748694Z","iopub.status.idle":"2025-07-02T04:17:14.678969Z","shell.execute_reply":"2025-07-02T04:17:14.677889Z"},"papermill":{"duration":5.937086,"end_time":"2025-07-02T04:17:14.680879","exception":false,"start_time":"2025-07-02T04:17:08.743793","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pycolmap","metadata":{"execution":{"iopub.execute_input":"2025-07-02T04:17:14.692121Z","iopub.status.busy":"2025-07-02T04:17:14.691731Z","iopub.status.idle":"2025-07-02T04:17:14.758656Z","shell.execute_reply":"2025-07-02T04:17:14.757887Z"},"papermill":{"duration":0.074539,"end_time":"2025-07-02T04:17:14.760322","exception":false,"start_time":"2025-07-02T04:17:14.685783","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport matplotlib.pyplot as plt\nimport plotly.graph_objects as go\nimport plotly.subplots as sp\nimport plotly.io as pio\npio.renderers.default = 'iframe'  \nfrom IPython.display import display, Image as IPImage\nimport ipywidgets as widgets\nfrom ipywidgets import interact, FloatSlider, Button, HBox, VBox\nfrom PIL import Image\nimport cv2\nfrom io import BytesIO\nimport base64\nimport os\nfrom scipy.spatial.transform import Rotation as R\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.optim import Adam\nimport shutil\nimport pycolmap\nfrom pathlib import Path\nimport cv2","metadata":{"execution":{"iopub.execute_input":"2025-07-02T04:17:14.771110Z","iopub.status.busy":"2025-07-02T04:17:14.770777Z","iopub.status.idle":"2025-07-02T04:17:21.264957Z","shell.execute_reply":"2025-07-02T04:17:21.263885Z"},"papermill":{"duration":6.501861,"end_time":"2025-07-02T04:17:21.266762","exception":false,"start_time":"2025-07-02T04:17:14.764901","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!mkdir images","metadata":{"execution":{"iopub.execute_input":"2025-07-02T04:17:21.277681Z","iopub.status.busy":"2025-07-02T04:17:21.277115Z","iopub.status.idle":"2025-07-02T04:17:21.409273Z","shell.execute_reply":"2025-07-02T04:17:21.407998Z"},"papermill":{"duration":0.139683,"end_time":"2025-07-02T04:17:21.411139","exception":false,"start_time":"2025-07-02T04:17:21.271456","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for dirname, _, filenames in os.walk('/kaggle/input/image-matching-challenge-2023/train/haiper/fountain/images'):\n    for filename in filenames:\n        path=os.path.join(dirname, filename)\n        img=Image.open(path)\n        #img = img.resize((img.size[0]//2,img.size[1]//2), Image.LANCZOS)\n        print(img.size)\n        img.save(os.path.join('./images',filename))\n     ","metadata":{"execution":{"iopub.execute_input":"2025-07-02T04:17:21.422644Z","iopub.status.busy":"2025-07-02T04:17:21.421620Z","iopub.status.idle":"2025-07-02T04:17:21.427028Z","shell.execute_reply":"2025-07-02T04:17:21.426101Z"},"papermill":{"duration":0.012645,"end_time":"2025-07-02T04:17:21.428475","exception":false,"start_time":"2025-07-02T04:17:21.415830","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"paths=[]\nfor dirname, _, filenames in os.walk('./images'):\n    for filename in filenames:\n        paths+=[os.path.join(dirname, filename)]\npaths.sort()","metadata":{"execution":{"iopub.execute_input":"2025-07-02T04:17:21.422644Z","iopub.status.busy":"2025-07-02T04:17:21.421620Z","iopub.status.idle":"2025-07-02T04:17:21.427028Z","shell.execute_reply":"2025-07-02T04:17:21.426101Z"},"papermill":{"duration":0.012645,"end_time":"2025-07-02T04:17:21.428475","exception":false,"start_time":"2025-07-02T04:17:21.415830","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"paths=[]\nfor dirname, _, filenames in os.walk('./images'):\n    for filename in filenames:\n        paths+=[os.path.join(dirname, filename)]\npaths.sort()\nprint(len(paths))\nprint(paths[0:5])","metadata":{"execution":{"iopub.execute_input":"2025-07-02T04:18:42.169621Z","iopub.status.busy":"2025-07-02T04:18:42.168578Z","iopub.status.idle":"2025-07-02T04:18:42.176822Z","shell.execute_reply":"2025-07-02T04:18:42.175767Z"},"papermill":{"duration":0.02179,"end_time":"2025-07-02T04:18:42.178341","exception":false,"start_time":"2025-07-02T04:18:42.156551","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from PIL import Image\nimport matplotlib.pyplot as plt\n\nn_cols = 4\nn_images = len(paths)\nn_rows = (n_images + n_cols - 1) // n_cols \n\nplt.figure(figsize=(16, 4 * n_rows))\nimages = []\n\nfor i, path in enumerate(paths):\n    img = Image.open(path)\n    images.append(img)\n    \n    plt.subplot(n_rows, n_cols, i + 1)\n    plt.imshow(img)\n    plt.axis('off')\n    plt.title(f'Image {i+1}')\n\nplt.tight_layout()\nplt.show()","metadata":{"execution":{"iopub.execute_input":"2025-07-02T04:18:42.199906Z","iopub.status.busy":"2025-07-02T04:18:42.199596Z","iopub.status.idle":"2025-07-02T04:19:42.175648Z","shell.execute_reply":"2025-07-02T04:19:42.174182Z"},"papermill":{"duration":61.449256,"end_time":"2025-07-02T04:19:43.637938","exception":false,"start_time":"2025-07-02T04:18:42.188682","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!mkdir sparse","metadata":{"execution":{"iopub.execute_input":"2025-07-02T04:19:45.030916Z","iopub.status.busy":"2025-07-02T04:19:45.030577Z","iopub.status.idle":"2025-07-02T04:19:45.182375Z","shell.execute_reply":"2025-07-02T04:19:45.181147Z"},"papermill":{"duration":0.852531,"end_time":"2025-07-02T04:19:45.184425","exception":false,"start_time":"2025-07-02T04:19:44.331894","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pycolmap\nfrom PIL import Image\nfrom pathlib import Path\n\n# Directory configuration\nIMAGE_DIR = Path('./images')\nDATABASE_PATH = Path('database.db')\nOUTPUT_DIR = Path('./sparse')\nOUTPUT_DIR.mkdir(exist_ok=True)\n\n# 1. Feature extraction (current API)\nsift_options = pycolmap.SiftExtractionOptions()\nsift_options.max_image_size = 1000  # Limit image size to reduce memory usage\nsift_options.max_num_features = 4096  # Limit the number of features\nsift_options.first_octave = 0  # Use default octave setting\n\ndevice = pycolmap.Device.auto  # Use GPU if available, otherwise CPU\n\nprint(f\"Starting feature extraction... (max_image_size: {sift_options.max_image_size})\")\npycolmap.extract_features(\n    database_path=str(DATABASE_PATH),\n    image_path=str(IMAGE_DIR),\n    sift_options=sift_options,\n    device=device\n)\nprint(\"Feature extraction completed.\")\n\n# 2. Feature matching (current API)\nmatching_options = pycolmap.SiftMatchingOptions()\nmatching_options.max_ratio = 0.8\nmatching_options.max_distance = 0.7\nmatching_options.cross_check = True\nmatching_options.max_num_matches = 16384  # Limit the number of matches\n\nprint(\"Starting feature matching...\")\npycolmap.match_exhaustive(\n    database_path=str(DATABASE_PATH),\n    sift_options=matching_options,\n    device=device\n)\nprint(\"Feature matching completed.\")\n\n# 3. Incremental mapping (current API)\npipeline_options = pycolmap.IncrementalPipelineOptions()\n\n# Set only available attributes safely\ntry:\n    if hasattr(pipeline_options, 'min_model_size'):\n        pipeline_options.min_model_size = 10\n    if hasattr(pipeline_options, 'max_model_overlap'):\n        pipeline_options.max_model_overlap = 20\n    if hasattr(pipeline_options, 'min_num_matches'):\n        pipeline_options.min_num_matches = 15\nexcept AttributeError as e:\n    print(f\"Some pipeline_options are unavailable: {e}\")\n    print(\"Using default settings.\")\n\n# Run mapping with pipeline options\ntry:\n    print(\"Running mapping with pipeline options...\")\n    maps = pycolmap.incremental_mapping(\n        database_path=str(DATABASE_PATH),\n        image_path=str(IMAGE_DIR),\n        output_path=str(OUTPUT_DIR),\n        options=pipeline_options\n    )\n    print(\"Mapping completed successfully!\")\nexcept Exception as e:\n    print(f\"Mapping with options failed: {e}\")\n    print(\"Retrying with default settings...\")\n    maps = pycolmap.incremental_mapping(\n        database_path=str(DATABASE_PATH),\n        image_path=str(IMAGE_DIR),\n        output_path=str(OUTPUT_DIR)\n    )\n\n# 4. Analyze results\nif maps and len(maps) > 0:\n    reconstruction = maps[0]\n\n    print(f\"Reconstruction successful! {len(reconstruction.images)} images produced {len(reconstruction.points3D)} 3D points.\")\n\n    # Get 3D points\n    if len(reconstruction.points3D) > 0:\n        points_3d = [point.xyz for point in reconstruction.points3D.values()]\n        points_xyz = np.array(points_3d)\n\n        # Compute center and radius of the point cloud\n        center = np.mean(points_xyz, axis=0)\n        distances = np.linalg.norm(points_xyz - center, axis=1)\n        radius = np.max(distances)\n\n        print(f\"Estimated scene radius: {radius:.2f} units\")\n        print(f\"Point cloud center: ({center[0]:.2f}, {center[1]:.2f}, {center[2]:.2f})\")\n        print(f\"Number of 3D points: {len(points_xyz)}\")\n\n    # Analyze camera positions\n    if len(reconstruction.images) > 0:\n        camera_positions = []\n        registered_count = 0\n        for image in reconstruction.images.values():\n            try:\n                R = image.rotmat()\n                t = image.tvec\n                if R is not None and t is not None:\n                    camera_center = -R.T @ t\n                    camera_positions.append(camera_center)\n                    registered_count += 1\n            except (AttributeError, ValueError):\n                continue\n\n        if camera_positions:\n            camera_positions = np.array(camera_positions)\n            camera_radii = np.linalg.norm(camera_positions, axis=1)\n\n            print(\"\\nCamera trajectory statistics:\")\n            print(f\"  - Registered cameras: {registered_count}\")\n            print(f\"  - Mean radius: {np.mean(camera_radii):.2f}\")\n            print(f\"  - Min radius: {np.min(camera_radii):.2f}\")\n            print(f\"  - Max radius: {np.max(camera_radii):.2f}\")\n\n    # Show camera model information\n    print(\"\\nCamera model information:\")\n    for camera_id, camera in reconstruction.cameras.items():\n        #print(f\"  Camera ID {camera_id}: {camera.model_name}\")\n        print(f\"    Resolution: {camera.width}x{camera.height}\")\n        print(f\"    Parameters: {camera.params}\")\n\n    # Save the reconstruction\n    reconstruction.write(OUTPUT_DIR)\n    print(f\"\\nReconstruction saved to {OUTPUT_DIR}\")\n\n    # Summary statistics\n    print(\"\\nDetailed statistics:\")\n    print(f\"  - Total images: {len(reconstruction.images)}\")\n    print(f\"  - Registered images: {registered_count}\")\n    print(f\"  - Total observations: {sum(len(img.points2D) for img in reconstruction.images.values() if hasattr(img, 'points2D'))}\")\n\n    try:\n        track_lengths = [\n            len(point.track.elements)\n            for point in reconstruction.points3D.values()\n            if hasattr(point, 'track') and hasattr(point.track, 'elements')\n        ]\n        if track_lengths:\n            print(f\"  - Mean track length: {np.mean(track_lengths):.1f}\")\n        else:\n            print(\"  - Mean track length: not available\")\n    except Exception as e:\n        print(f\"  - Mean track length: error ({e})\")\n\nelse:\n    print(\"Reconstruction failed!\")\n    print(\"Please check the following:\")\n    print(\"1. Are there enough images in the image directory?\")\n    print(\"2. Do the images have sufficient overlap?\")\n    print(\"3. Are the images of good quality (not blurry or shaky)?\")\n    print(\"4. Was the database file created properly?\")\n\n# Optional: Debug information\nprint(\"\\nDebug information:\")\nprint(f\"pycolmap version: {pycolmap.__version__ if hasattr(pycolmap, '__version__') else 'Unknown'}\")\nprint(f\"Image directory: {IMAGE_DIR}\")\nprint(f\"Database path: {DATABASE_PATH}\")\nprint(f\"Output directory: {OUTPUT_DIR}\")\n\n# Show available attributes of IncrementalPipelineOptions\nprint(\"\\nAvailable attributes in IncrementalPipelineOptions:\")\npipeline_test = pycolmap.IncrementalPipelineOptions()\nfor attr in dir(pipeline_test):\n    if not attr.startswith('_'):\n        try:\n            value = getattr(pipeline_test, attr)\n            if callable(value):\n                print(f\"  - {attr}(): method\")\n            else:\n                print(f\"  - {attr}: {value}\")\n        except:\n            print(f\"  - {attr}: (unavailable)\")\n\ndel pipeline_test\n\n# Show available attributes of Image object\nif 'reconstruction' in locals() and reconstruction and len(reconstruction.images) > 0:\n    print(\"\\nAvailable attributes of Image object:\")\n    sample_image = next(iter(reconstruction.images.values()))\n    for attr in dir(sample_image):\n        if not attr.startswith('_'):\n            try:\n                value = getattr(sample_image, attr)\n                if callable(value):\n                    print(f\"  - {attr}(): method\")\n                else:\n                    print(f\"  - {attr}: {type(value).__name__}\")\n            except:\n                print(f\"  - {attr}: (unavailable)\")\n\n# Check basic database statistics\ntry:\n    import sqlite3\n    conn = sqlite3.connect(DATABASE_PATH)\n    cursor = conn.cursor()\n\n    cursor.execute(\"SELECT COUNT(*) FROM images\")\n    image_count = cursor.fetchone()[0]\n\n    cursor.execute(\"SELECT COUNT(*) FROM keypoints\")\n    keypoint_count = cursor.fetchone()[0]\n\n    cursor.execute(\"SELECT COUNT(*) FROM matches\")\n    match_count = cursor.fetchone()[0]\n\n    print(\"\\nDatabase statistics:\")\n    print(f\"  - Number of images: {image_count}\")\n    print(f\"  - Number of keypoints: {keypoint_count}\")\n    print(f\"  - Number of matches: {match_count}\")\n\n    conn.close()\nexcept Exception as e:\n    print(f\"Failed to retrieve database statistics: {e}\")\n","metadata":{"execution":{"iopub.execute_input":"2025-07-02T04:19:47.955395Z","iopub.status.busy":"2025-07-02T04:19:47.955032Z","iopub.status.idle":"2025-07-02T04:29:08.700104Z","shell.execute_reply":"2025-07-02T04:29:08.698880Z"},"papermill":{"duration":561.446623,"end_time":"2025-07-02T04:29:08.701709","exception":false,"start_time":"2025-07-02T04:19:47.255086","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nfrom mpl_toolkits.mplot3d import Axes3D\n\n# Load COLMAP reconstruction\nreconstruction = pycolmap.Reconstruction(\"sparse/\")  # Specify the sparse folder\nprint(reconstruction)","metadata":{"execution":{"iopub.execute_input":"2025-07-02T04:29:10.096228Z","iopub.status.busy":"2025-07-02T04:29:10.095650Z","iopub.status.idle":"2025-07-02T04:29:10.122739Z","shell.execute_reply":"2025-07-02T04:29:10.121768Z"},"papermill":{"duration":0.730982,"end_time":"2025-07-02T04:29:10.124395","exception":false,"start_time":"2025-07-02T04:29:09.393413","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Print camera positions\nfor image_id, image in reconstruction.images.items():\n    cam_fromworld = image.cam_from_world()\n    print(cam_fromworld.translation)\n    print(cam_fromworld.rotation.matrix())\n    print()","metadata":{"execution":{"iopub.execute_input":"2025-07-02T04:29:11.545013Z","iopub.status.busy":"2025-07-02T04:29:11.544585Z","iopub.status.idle":"2025-07-02T04:29:11.582351Z","shell.execute_reply":"2025-07-02T04:29:11.581379Z"},"papermill":{"duration":0.754477,"end_time":"2025-07-02T04:29:11.584079","exception":false,"start_time":"2025-07-02T04:29:10.829602","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"    #bad old form\n    for image_id, image in reconstruction.images.items():\n        print(image.cam_from_world().translation)","metadata":{"execution":{"iopub.execute_input":"2025-07-01T05:09:25.395318Z","iopub.status.busy":"2025-07-01T05:09:25.394588Z","iopub.status.idle":"2025-07-01T05:09:25.431539Z","shell.execute_reply":"2025-07-01T05:09:25.430332Z","shell.execute_reply.started":"2025-07-01T05:09:25.395284Z"},"papermill":{"duration":0.759943,"end_time":"2025-07-02T04:29:13.066347","exception":false,"start_time":"2025-07-02T04:29:12.306404","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Prepare a 3D plot\nfig = plt.figure(figsize=(10, 8))\nax = fig.add_subplot(111, projection='3d')\n\n# Plot the camera positions and orientations\nfor image_id, image in reconstruction.images.items():\n    # Get camera position (cam_from_world.translation())\n    cam_fromworld = image.cam_from_world()\n    camera_pos = cam_fromworld.translation\n    ax.scatter(camera_pos[0], camera_pos[1], camera_pos[2], c='red', marker='o')\n\n    # Compute camera orientation (from rotation matrix)\n    rotation_matrix = cam_fromworld.rotation.matrix()\n    forward_dir = rotation_matrix @ np.array([0, 0, 1])  # Forward vector of the camera\n    ax.quiver(\n        camera_pos[0], camera_pos[1], camera_pos[2],\n        forward_dir[0], forward_dir[1], forward_dir[2],\n        length=0.4, color='blue', arrow_length_ratio=0.1\n    )\n\n# Axis labels\nax.set_xlabel('X')\nax.set_ylabel('Y')\nax.set_zlabel('Z')\nax.set_title('Camera Positions and Directions')\nplt.show()","metadata":{"execution":{"iopub.execute_input":"2025-07-02T04:29:14.487873Z","iopub.status.busy":"2025-07-02T04:29:14.486847Z","iopub.status.idle":"2025-07-02T04:29:16.591959Z","shell.execute_reply":"2025-07-02T04:29:16.590851Z"},"papermill":{"duration":2.82686,"end_time":"2025-07-02T04:29:16.593856","exception":false,"start_time":"2025-07-02T04:29:13.766996","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"reconstruction = pycolmap.Reconstruction(\"sparse/\")\npoints_3d = []\nfor point3D_id, point3D in reconstruction.points3D.items():\n    points_3d.append(point3D.xyz)  # [x, y, z] \n\npoints_3d = np.array(points_3d)  \nprint(f\"Number of 3D points: {len(points_3d)}\")\nprint(f\"Coordinates of the first point: {points_3d[0]}\")","metadata":{"execution":{"iopub.execute_input":"2025-07-02T04:29:18.007172Z","iopub.status.busy":"2025-07-02T04:29:18.006738Z","iopub.status.idle":"2025-07-02T04:29:18.071078Z","shell.execute_reply":"2025-07-02T04:29:18.070121Z"},"papermill":{"duration":0.771364,"end_time":"2025-07-02T04:29:18.072654","exception":false,"start_time":"2025-07-02T04:29:17.301290","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nfrom mpl_toolkits.mplot3d import Axes3D\n\nfig = plt.figure(figsize=(10, 8))\nax = fig.add_subplot(111, projection='3d')\n\nax.scatter(\n    points_3d[:,0],  # X\n    points_3d[:,1],  # Y\n    points_3d[:,2],  # Z\n    s=1,  \n    c='blue',  \n    alpha=0.5,  \n    label=\"Estimated Object\"\n)\n\ncamera_plotted = False\nfor image in reconstruction.images.values():\n    cam_fromworld=image.cam_from_world()\n    cam_pos=cam_fromworld.translation\n    if not camera_plotted:\n        ax.scatter(cam_pos[0], cam_pos[1], cam_pos[2], c='red', marker='^', label=\"Camera\")\n        camera_plotted = True\n    else:\n        ax.scatter(cam_pos[0], cam_pos[1], cam_pos[2], c='red', marker='^')\n\n#ax.set_xlim(-20,20)\n#ax.set_ylim(-20,20)\n#ax.set_zlim(-10,60)\nax.set_xlabel('X')\nax.set_ylabel('Y')\nax.set_zlabel('Z')\n\nax.legend(bbox_to_anchor=(1.05, 1), loc='upper left')\n\nplt.title(\"Estimated Object + Camera\")\nplt.tight_layout()  \nplt.show()","metadata":{"execution":{"iopub.execute_input":"2025-07-02T04:29:19.488310Z","iopub.status.busy":"2025-07-02T04:29:19.486923Z","iopub.status.idle":"2025-07-02T04:29:20.411088Z","shell.execute_reply":"2025-07-02T04:29:20.409911Z"},"papermill":{"duration":1.628004,"end_time":"2025-07-02T04:29:20.417466","exception":false,"start_time":"2025-07-02T04:29:18.789462","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"    import imageio.v2 as imageio\n    output_file = \"animation.gif\"\n    imageio.mimsave(output_file, images, duration=0.2, loop=0) \n    from IPython.display import Image\n    Image(open(output_file,'rb').read())","metadata":{"papermill":{"duration":0.702776,"end_time":"2025-07-02T04:29:21.823863","exception":false,"start_time":"2025-07-02T04:29:21.121087","status":"completed"},"tags":[]}},{"cell_type":"markdown","source":"","metadata":{"papermill":{"duration":0.717189,"end_time":"2025-07-02T04:29:23.270941","exception":false,"start_time":"2025-07-02T04:29:22.553752","status":"completed"},"tags":[]}},{"cell_type":"markdown","source":"","metadata":{"papermill":{"duration":0.724282,"end_time":"2025-07-02T04:29:24.697078","exception":false,"start_time":"2025-07-02T04:29:23.972796","status":"completed"},"tags":[]}}]}