{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":71885,"databundleVersionId":8143495,"sourceType":"competition"},{"sourceId":7884485,"sourceType":"datasetVersion","datasetId":4628051},{"sourceId":172469456,"sourceType":"kernelVersion"},{"sourceId":4534,"sourceType":"modelInstanceVersion","modelInstanceId":3326},{"sourceId":17191,"sourceType":"modelInstanceVersion","modelInstanceId":14317},{"sourceId":17555,"sourceType":"modelInstanceVersion","modelInstanceId":14611}],"dockerImageVersionId":30684,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# SETUP","metadata":{}},{"cell_type":"code","source":"from IPython.display import clear_output\n\n!pip install -r /kaggle/input/check-image-orientation/requirements.txt\n!pip install --no-index /kaggle/input/imc2024-packages-lightglue-rerun-kornia/* --no-deps\n!mkdir -p /root/.cache/torch/hub/checkpoints\n!cp /kaggle/input/aliked/pytorch/aliked-n16/1/* /root/.cache/torch/hub/checkpoints/\n!cp /kaggle/input/lightglue/pytorch/aliked/1/* /root/.cache/torch/hub/checkpoints/\n!cp /kaggle/input/lightglue/pytorch/aliked/1/aliked_lightglue.pth /root/.cache/torch/hub/checkpoints/aliked_lightglue_v0-1_arxiv-pth\n!cp /kaggle/input/check-image-orientation/2020-11-16_resnext50_32x4d.zip /root/.cache/torch/hub/checkpoints/\n\nclear_output(wait=False)","metadata":{"_uuid":"085d1ffd-0238-4af9-aee2-16500ee2a223","_cell_guid":"0cdd81c0-04bc-42f7-b697-671da238b617","collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2024-04-15T09:37:26.340737Z","iopub.execute_input":"2024-04-15T09:37:26.341083Z","iopub.status.idle":"2024-04-15T09:37:31.661166Z","shell.execute_reply.started":"2024-04-15T09:37:26.341059Z","shell.execute_reply":"2024-04-15T09:37:31.660067Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"from pathlib import Path\nfrom copy import deepcopy\nimport numpy as np\nimport math\nimport pandas as pd\nimport pandas.api.types\nfrom itertools import combinations\nimport sys, torch, h5py, pycolmap, datetime\nfrom PIL import Image\nfrom pathlib import Path\nimport torch.nn.functional as F\nimport torchvision.transforms.functional as TF\nimport kornia as K\nimport kornia.feature as KF\nfrom lightglue.utils import load_image\nfrom lightglue import LightGlue, ALIKED, match_pair\nfrom transformers import AutoImageProcessor, AutoModel\nfrom check_orientation.pre_trained_models import create_model\nsys.path.append(\"/kaggle/input/colmap-db-import\")\nfrom database import *\nfrom h5_to_db import *\n\nIMC_PATH = '/kaggle/input/image-matching-challenge-2024'\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nclear_output(wait=False)","metadata":{"_uuid":"2ca8bfe2-ceb8-4c67-8bf4-69dee8ad81b2","_cell_guid":"9845a075-97c8-48f4-b50f-ad12fcbfa74f","collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2024-04-15T09:37:36.322876Z","iopub.execute_input":"2024-04-15T09:37:36.323231Z","iopub.status.idle":"2024-04-15T09:37:57.458921Z","shell.execute_reply.started":"2024-04-15T09:37:36.323203Z","shell.execute_reply":"2024-04-15T09:37:57.457857Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# CHECK IMAGE ORIENTATION","metadata":{}},{"cell_type":"code","source":"def rotate_image(image,rotation):\n    for i in range(4):\n        with torch.no_grad():\n            pred = rotation(image[None,...]).argmax()\n        if pred == 0: break\n        image = image.rot90(dims=[1,2])\n    return image","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# OVERLAP DETECTION","metadata":{}},{"cell_type":"code","source":"def overlap_detection(extractor, matcher, image0, image1, min_matches):\n    feats0, feats1, matches01 = match_pair(extractor, matcher, image0, image1)\n    if len(matches01['matches']) < min_matches:\n        return feats0, feats1, matches01\n    kpts0, kpts1, matches = feats0[\"keypoints\"], feats1[\"keypoints\"], matches01[\"matches\"]\n    m_kpts0, m_kpts1 = kpts0[matches[..., 0]], kpts1[matches[..., 1]]\n    left0, top0 = m_kpts0.numpy().min(axis=0).astype(int)\n    width0, height0 = m_kpts0.numpy().max(axis=0).astype(int)\n    height0 -= top0\n    width0 -= left0\n    left1, top1 = m_kpts1.numpy().min(axis=0).astype(int)\n    width1, height1 = m_kpts1.numpy().max(axis=0).astype(int)\n    height1 -= top1\n    width1 -= left1\n    crop_box0 = (top0, left0, height0, width0)\n    crop_box1 = (top1, left1, height1, width1)\n    cropped_img_tensor0 = TF.crop(image0, *crop_box0)\n    cropped_img_tensor1 = TF.crop(image1, *crop_box1)\n    feats0_c, feats1_c, matches01_c = match_pair(extractor, matcher, cropped_img_tensor0, cropped_img_tensor1)\n    feats0_c['keypoints'][...,0] += left0\n    feats0_c['keypoints'][...,1] += top0\n    feats1_c['keypoints'][...,0] += left1\n    feats1_c['keypoints'][...,1] += top1\n    return feats0_c, feats1_c, matches01_c","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# SUBMISSION","metadata":{}},{"cell_type":"code","source":"def reset_seed(seed):\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    np.random.seed(seed)\n\ndef parse_sample_submission(data_path):\n    data_dict = {}\n    with open(data_path, \"r\") as f:\n        for i, l in enumerate(f):\n            if i == 0:\n                print(\"header:\", l)\n\n            if l and i > 0:\n                image_path, dataset, scene, _, _ = l.strip().split(',')\n                if dataset not in data_dict:\n                    data_dict[dataset] = {}\n                if scene not in data_dict[dataset]:\n                    data_dict[dataset][scene] = []\n                data_dict[dataset][scene].append(Path(IMC_PATH +'/'+ image_path))\n\n    for dataset in data_dict:\n        for scene in data_dict[dataset]:\n            print(f\"{dataset} / {scene} -> {len(data_dict[dataset][scene])} images\")\n\n    return data_dict\n\ndef arr_to_str(a):\n    return \";\".join([str(x) for x in a.reshape(-1)])","metadata":{"_uuid":"1a935a33-c8c9-4e79-9b24-7aadbe88e740","_cell_guid":"19a390a5-0c4d-49a6-ad3e-e803fc5cce90","collapsed":false,"jupyter":{"outputs_hidden":false},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def create_submission(results,data_dict,base_path):    \n    with open(\"submission.csv\", \"w\") as f:\n        f.write(\"image_path,dataset,scene,rotation_matrix,translation_vector\\n\")\n        \n        for dataset in data_dict:\n            if dataset in results:\n                res = results[dataset]\n            else:\n                res = {}\n            \n            for scene in data_dict[dataset]:\n                if scene in res:\n                    scene_res = res[scene]\n                else:\n                    scene_res = {\"R\":{}, \"t\":{}}\n                    \n                for image in data_dict[dataset][scene]:\n                    if image in scene_res:\n                        R = scene_res[image][\"R\"].reshape(-1)\n                        T = scene_res[image][\"t\"].reshape(-1)\n                    else:\n                        R = np.eye(3).reshape(-1)\n                        T = np.zeros((3))\n                    image_path = str(image.relative_to(base_path))\n                    f.write(f\"{image_path},{dataset},{scene},{arr_to_str(R)},{arr_to_str(T)}\\n\")","metadata":{"_uuid":"dad1dfb0-4aa9-4163-b9ea-d2653d0888ea","_cell_guid":"27c7bb8e-5a45-45f1-9ac4-faf8f78216cf","collapsed":false,"jupyter":{"outputs_hidden":false},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def run(data_path,get_pairs,keypoints_matches,ransac_and_sparse_reconstruction,submit=True):\n    results = {}\n    \n    data_dict = parse_sample_submission(data_path)\n    datasets = list(data_dict.keys())\n    \n    for dataset in datasets:\n        if dataset not in results:\n            results[dataset] = {}\n            \n        for scene in data_dict[dataset]:\n            images_dir = data_dict[dataset][scene][0].parent\n            results[dataset][scene] = {}\n            image_paths = data_dict[dataset][scene]\n\n            index_pairs = get_pairs(image_paths)\n            keypoints_matches(image_paths,index_pairs)                \n            maps = ransac_and_sparse_reconstruction(image_paths[0].parent)\n            clear_output(wait=False)\n            \n            path = 'test' if submit else 'train'\n            images_registered  = 0\n            best_idx = 0\n            for idx, rec in maps.items():\n                if len(rec.images) > images_registered:\n                    images_registered = len(rec.images)\n                    best_idx = idx\n                    \n            for k, im in maps[best_idx].images.items():\n                key = Path(IMC_PATH) / path / scene / \"images\" / im.name\n                results[dataset][scene][key] = {}\n                results[dataset][scene][key][\"R\"] = deepcopy(im.cam_from_world.rotation.matrix())\n                results[dataset][scene][key][\"t\"] = deepcopy(np.array(im.cam_from_world.translation))\n\n            create_submission(results, data_dict, Path(IMC_PATH))","metadata":{"_uuid":"68f0a367-8655-444e-a38e-567c6aa3911f","_cell_guid":"23d92c5b-13b2-4420-87a6-6037747fd98c","collapsed":false,"jupyter":{"outputs_hidden":false},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# mAA METRIC","metadata":{}},{"cell_type":"code","source":"_EPS = np.finfo(float).eps * 4.0\n\n# mAA evaluation thresholds per scene, different accoring to the scene\ntranslation_thresholds_meters_dict = {\n 'multi-temporal-temple-baalshamin':  np.array([0.025,  0.05,  0.1,  0.2,  0.5,  1.0]),\n 'pond':                              np.array([0.025,  0.05,  0.1,  0.2,  0.5,  1.0]),\n 'transp_obj_glass_cylinder':         np.array([0.0025, 0.005, 0.01, 0.02, 0.05, 0.1]),\n 'transp_obj_glass_cup':              np.array([0.0025, 0.005, 0.01, 0.02, 0.05, 0.1]),\n 'church':                            np.array([0.025,  0.05,  0.1,  0.2,  0.5,  1.0]),\n 'lizard':                            np.array([0.025,  0.05,  0.1,  0.2,  0.5,  1.0]),\n 'dioscuri':                          np.array([0.025,  0.05,  0.1,  0.2,  0.5,  1.0]), \n}\n\n\ndef vector_norm(data, axis=None, out=None):\n    '''Return length, i.e. Euclidean norm, of ndarray along axis.'''\n    data = np.array(data, dtype=np.float64, copy=True)\n    if out is None:\n        if data.ndim == 1:\n            return math.sqrt(np.dot(data, data))\n        data *= data\n        out = np.atleast_1d(np.sum(data, axis=axis))\n        np.sqrt(out, out)\n        return out\n    data *= data\n    np.sum(data, axis=axis, out=out)\n    np.sqrt(out, out)\n    return None\n\n\ndef quaternion_matrix(quaternion):\n    '''Return homogeneous rotation matrix from quaternion.'''\n    q = np.array(quaternion, dtype=np.float64, copy=True)\n    n = np.dot(q, q)\n    if n < _EPS:\n        # print(\"special case\")\n        return np.identity(4)\n    q *= math.sqrt(2.0 / n)\n    q = np.outer(q, q)\n    return np.array(\n        [\n            [\n                1.0 - q[2, 2] - q[3, 3],\n                q[1, 2] - q[3, 0],\n                q[1, 3] + q[2, 0],\n                0.0,\n            ],\n            [\n                q[1, 2] + q[3, 0],\n                1.0 - q[1, 1] - q[3, 3],\n                q[2, 3] - q[1, 0],\n                0.0,\n            ],\n            [\n                q[1, 3] - q[2, 0],\n                q[2, 3] + q[1, 0],\n                1.0 - q[1, 1] - q[2, 2],\n                0.0,\n            ],\n            [0.0, 0.0, 0.0, 1.0],\n        ]\n    )\n\n\n# based on the 3D registration from https://github.com/cgohlke/transformations\ndef affine_matrix_from_points(v0, v1, shear=False, scale=True, usesvd=True):\n    \n    v0 = np.array(v0, dtype=np.float64, copy=True)\n    v1 = np.array(v1, dtype=np.float64, copy=True)\n\n    ndims = v0.shape[0]\n    if ndims < 2 or v0.shape[1] < ndims or v0.shape != v1.shape:\n        raise ValueError(\"input arrays are of wrong shape or type\")\n\n    # move centroids to origin\n    t0 = -np.mean(v0, axis=1)\n    M0 = np.identity(ndims + 1)\n    M0[:ndims, ndims] = t0\n    v0 += t0.reshape(ndims, 1)\n    t1 = -np.mean(v1, axis=1)\n    M1 = np.identity(ndims + 1)\n    M1[:ndims, ndims] = t1\n    v1 += t1.reshape(ndims, 1)\n\n    if shear:\n        # Affine transformation\n        A = np.concatenate((v0, v1), axis=0)\n        u, s, vh = np.linalg.svd(A.T)\n        vh = vh[:ndims].T\n        B = vh[:ndims]\n        C = vh[ndims: 2 * ndims]\n        t = np.dot(C, np.linalg.pinv(B))\n        t = np.concatenate((t, np.zeros((ndims, 1))), axis=1)\n        M = np.vstack((t, ((0.0,) * ndims) + (1.0,)))\n    elif usesvd or ndims != 3:\n        # Rigid transformation via SVD of covariance matrix\n        u, s, vh = np.linalg.svd(np.dot(v1, v0.T))\n        # rotation matrix from SVD orthonormal bases\n        R = np.dot(u, vh)\n        if np.linalg.det(R) < 0.0:\n            # R does not constitute right handed system\n            R -= np.outer(u[:, ndims - 1], vh[ndims - 1, :] * 2.0)\n            s[-1] *= -1.0\n        # homogeneous transformation matrix\n        M = np.identity(ndims + 1)\n        M[:ndims, :ndims] = R\n    else:\n        # Rigid transformation matrix via quaternion\n        # compute symmetric matrix N\n        xx, yy, zz = np.sum(v0 * v1, axis=1)\n        xy, yz, zx = np.sum(v0 * np.roll(v1, -1, axis=0), axis=1)\n        xz, yx, zy = np.sum(v0 * np.roll(v1, -2, axis=0), axis=1)\n        N = [\n            [xx + yy + zz, 0.0, 0.0, 0.0],\n            [yz - zy, xx - yy - zz, 0.0, 0.0],\n            [zx - xz, xy + yx, yy - xx - zz, 0.0],\n            [xy - yx, zx + xz, yz + zy, zz - xx - yy],\n        ]\n        # quaternion: eigenvector corresponding to most positive eigenvalue\n        w, V = np.linalg.eigh(N)\n        q = V[:, np.argmax(w)]\n        # print (vector_norm(q), np.linalg.norm(q))\n        q /= vector_norm(q)  # unit quaternion\n        # homogeneous transformation matrix\n        M = quaternion_matrix(q)\n\n    if scale and not shear:\n        # Affine transformation; scale is ratio of RMS deviations from centroid\n        v0 *= v0\n        v1 *= v1\n        M[:ndims, :ndims] *= math.sqrt(np.sum(v1) / np.sum(v0))\n\n    # move centroids back\n    M = np.dot(np.linalg.inv(M1), np.dot(M, M0))\n    M /= M[ndims, ndims]\n\n    # print(\"transformation matrix Python Script: \", M)\n\n    return M\n\n\n# This is the IMC 3D error metric code\ndef register_by_Horn(ev_coord, gt_coord, ransac_threshold, inl_cf, strict_cf):\n    \n    # remove invalid cameras, the index is returned\n    idx_cams = np.all(np.isfinite(ev_coord), axis=0)\n    ev_coord = ev_coord[:, idx_cams]\n    gt_coord = gt_coord[:, idx_cams]\n\n    # initialization\n    n = ev_coord.shape[1]\n    r = ransac_threshold.shape[0]\n    ransac_threshold = np.expand_dims(ransac_threshold, axis=0)\n    ransac_threshold2 = ransac_threshold**2\n    ev_coord_1 = np.vstack((ev_coord, np.ones(n)))\n\n    max_no_inl = np.zeros((1, r))\n    best_inl_err = np.full(r, np.Inf)\n    best_transf_matrix = np.zeros((r, 4, 4))\n    best_err = np.full((n, r), np.Inf)\n    strict_inl = np.full((n, r), False)\n    triplets_used = np.zeros((3, r))\n\n    # run on camera triplets\n    for ii in range(n-2):\n        for jj in range(ii+1, n-1):\n            for kk in range(jj+1, n):\n                i = [ii, jj, kk]\n                triplets_used_now = np.full((n), False)\n                triplets_used_now[i] = True\n                # if both ii, jj, kk are strict inliers for the best current model just skip\n                if np.all(strict_inl[i]):\n                    continue\n                # get transformation T by Horn on the triplet camera center correspondences\n                transf_matrix = affine_matrix_from_points(ev_coord[:, i], gt_coord[:, i], usesvd=False)\n                # apply transformation T to test camera centres\n                rotranslated = np.matmul(transf_matrix[:3], ev_coord_1)\n                # compute error and inliers\n                err = np.sum((rotranslated - gt_coord)**2, axis=0)\n                inl = np.expand_dims(err, axis=1) < ransac_threshold2\n                no_inl = np.sum(inl, axis=0)\n                # if the number of inliers is close to that of the best model so far, go for refinement\n                to_ref = np.squeeze(((no_inl > 2) & (no_inl > max_no_inl * inl_cf)), axis=0)\n                for q in np.argwhere(to_ref):                        \n                    qq = q[0]\n                    if np.any(np.all((np.expand_dims(inl[:, qq], axis=1) == inl[:, :qq]), axis=0)):\n                        # already done for this set of inliers\n                        continue\n                    # get transformation T by Horn on the inlier camera center correspondences\n                    transf_matrix = affine_matrix_from_points(ev_coord[:, inl[:, qq]], gt_coord[:, inl[:, qq]])\n                    # apply transformation T to test camera centres\n                    rotranslated = np.matmul(transf_matrix[:3], ev_coord_1)\n                    # compute error and inliers\n                    err_ref = np.sum((rotranslated - gt_coord)**2, axis=0)\n                    err_ref_sum = np.sum(err_ref, axis=0)\n                    err_ref = np.expand_dims(err_ref, axis=1)\n                    inl_ref = err_ref < ransac_threshold2\n                    no_inl_ref = np.sum(inl_ref, axis=0)\n                    # update the model if better for each threshold\n                    to_update = np.squeeze((no_inl_ref > max_no_inl) | ((no_inl_ref == max_no_inl) & (err_ref_sum < best_inl_err)), axis=0)\n                    if np.any(to_update):\n                        triplets_used[0, to_update] = ii\n                        triplets_used[1, to_update] = jj\n                        triplets_used[2, to_update] = kk\n                        max_no_inl[:, to_update] = no_inl_ref[to_update]\n                        best_err[:, to_update] = np.sqrt(err_ref)\n                        best_inl_err[to_update] = err_ref_sum\n                        strict_inl[:, to_update] = (best_err[:, to_update] < strict_cf * ransac_threshold[:, to_update])\n                        best_transf_matrix[to_update] = transf_matrix\n\n#     for i in range(r):\n#        print(f'Registered cameras {int(max_no_inl[0, i])}/{n} for threshold {ransac_threshold[0, i]}')\n\n    best_model = {\n        \"valid_cams\": idx_cams,        \n        \"no_inl\": max_no_inl,\n        \"err\": best_err,\n        \"triplets_used\": triplets_used,\n        \"transf_matrix\": best_transf_matrix}\n    return best_model\n\n\n# mAA computation\ndef mAA_on_cameras(err, thresholds, n, skip_top_thresholds, to_dec=3):\n    \n    aux = err[:, skip_top_thresholds:] < np.expand_dims(np.asarray(thresholds[skip_top_thresholds:]), axis=0)\n    return np.sum(np.maximum(np.sum(aux, axis=0) - to_dec, 0)) / (len(thresholds[skip_top_thresholds:]) * (n - to_dec))\n\n\n# import data - no error handling in case float(x) fails\ndef get_camera_centers_from_df(df):\n    out = {}\n    for row in df.iterrows():\n        row = row[1]\n        fname = row['image_path']\n        R = np.array([float(x) for x in (row['rotation_matrix'].split(';'))]).reshape(3,3)\n        t = np.array([float(x) for x in (row['translation_vector'].split(';'))]).reshape(3)\n        center = -R.T @ t\n        out[fname] = center\n    return out\n\n\ndef evaluate_rec(gt_df, user_df, inl_cf = 0.8, strict_cf=0.5, skip_top_thresholds=2, to_dec=3,\n                 thresholds=[0.005, 0.01, 0.02, 0.03, 0.04, 0.05, 0.1, 0.15, 0.2]):\n    # get camera centers\n    ucameras = get_camera_centers_from_df(user_df)\n    gcameras = get_camera_centers_from_df(gt_df)    \n\n    # the denominator for mAA ratio\n    m = gt_df.shape[0]\n    \n    # get the image list to use\n    good_cams = []\n    for image_path in gcameras.keys():\n        if image_path in ucameras.keys():\n            good_cams.append(image_path)\n        \n    # put corresponding camera centers into matrices\n    n = len(good_cams)\n    u_cameras = np.zeros((3, n))\n    g_cameras = np.zeros((3, n))\n    \n    ii = 0\n    for i in good_cams:\n        u_cameras[:, ii] = ucameras[i]\n        g_cameras[:, ii] = gcameras[i]\n        ii += 1\n        \n    # Horn camera centers registration, a different best model for each camera threshold\n    model = register_by_Horn(u_cameras, g_cameras, np.asarray(thresholds), inl_cf, strict_cf)\n    \n    # transformation matrix\n#     print(\"\\nTransformation matrix for maximum threshold\")\n    T = np.squeeze(model['transf_matrix'][-1])\n#     print(T)\n    \n    # mAA\n    mAA = mAA_on_cameras(model[\"err\"], thresholds, m, skip_top_thresholds, to_dec)\n    # print(f'mAA = {mAA * 100 : .2f}% considering {m} input cameras - {to_dec}')\n    return mAA\n\n\ndef score(solution: pd.DataFrame, submission: pd.DataFrame) -> float:\n    \n    scenes = list(set(solution['dataset'].tolist()))\n    results_per_dataset = []\n    for dataset in scenes:\n        print(f\"\\n*** {dataset} ***\")\n#         start = time.time()\n        gt_ds = solution[solution['dataset'] == dataset]\n        user_ds = submission[submission['dataset'] == dataset]\n        gt_ds = gt_ds.sort_values(by=['image_path'], ascending = True)\n        user_ds = user_ds.sort_values(by=['image_path'], ascending = True)\n        result = evaluate_rec(gt_ds, user_ds, inl_cf=0, strict_cf=-1, skip_top_thresholds=0, to_dec=3,\n                 thresholds=translation_thresholds_meters_dict[dataset])\n#         end = time.time()\n        print(f\"\\nmAA: {round(result,4)}\")\n#         print(\"Running time: %s\" % (end - start))        \n        results_per_dataset.append(result)\n    return float(np.array(results_per_dataset).mean())","metadata":{},"execution_count":null,"outputs":[]}]}