{ "cells": [ { "cell_type": "code", "execution_count": 1, "id": "65c62d87", "metadata": {}, "outputs": [], "source": [ "from _collections_abc import dict_values\n", "from math import isnan\n", "from pathlib import Path\n", "from re import L\n", "import awkward as ak\n", "from typing import (\n", " Any,\n", " Generator,\n", " Iterable,\n", " Optional,\n", " Sequence,\n", " TypeAlias,\n", " TypedDict,\n", " cast,\n", " TypeVar,\n", ")\n", "from datetime import datetime, timedelta\n", "from jaxtyping import Array, Float, Num, jaxtyped\n", "import numpy as np\n", "from cv2 import undistortPoints\n", "from app.camera import Camera, CameraParams, Detection\n", "import jax.numpy as jnp\n", "from beartype import beartype\n", "from scipy.spatial.transform import Rotation as R\n", "from app.camera import (\n", " Camera,\n", " CameraID,\n", " CameraParams,\n", " Detection,\n", " calculate_affinity_matrix_by_epipolar_constraint,\n", " classify_by_camera,\n", ")\n", "from copy import copy as shallow_copy\n", "import jax\n", "from beartype.typing import Mapping, Sequence\n", "from itertools import chain\n", "import orjson\n", "from app.visualize.whole_body import visualize_whole_body\n", "from matplotlib import pyplot as plt" ] }, { "cell_type": "code", "execution_count": 2, "id": "74ec95dd", "metadata": {}, "outputs": [], "source": [ "NDArray: TypeAlias = np.ndarray\n", "DetectionGenerator: TypeAlias = Generator[Detection, None, None]\n", "\n", "DELTA_T_MIN = timedelta(milliseconds=1)\n", "\"\"\"所有类型\"\"\"\n", "\n", "T = TypeVar(\"T\")\n", "\n", "\n", "def unwrap(val: Optional[T]) -> T:\n", " if val is None:\n", " raise ValueError(\"None\")\n", " return val\n", "\n", "\n", "class KeypointDataset(TypedDict):\n", " frame_index: int\n", " boxes: Num[NDArray, \"N 4\"]\n", " kps: Num[NDArray, \"N J 2\"]\n", " kps_scores: Num[NDArray, \"N J\"]\n", "\n", "\n", "class Resolution(TypedDict):\n", " width: int\n", " height: int\n", "\n", "\n", "class Intrinsic(TypedDict):\n", " camera_matrix: Num[Array, \"3 3\"]\n", " \"\"\"\n", " K\n", " \"\"\"\n", " distortion_coefficients: Num[Array, \"N\"]\n", " \"\"\"\n", " distortion coefficients; usually 5\n", " \"\"\"\n", "\n", "\n", "class Extrinsic(TypedDict):\n", " rvec: Num[NDArray, \"3\"]\n", " tvec: Num[NDArray, \"3\"]\n", "\n", "\n", "class ExternalCameraParams(TypedDict):\n", " name: str\n", " port: int\n", " intrinsic: Intrinsic\n", " extrinsic: Extrinsic\n", " resolution: Resolution\n" ] }, { "cell_type": "code", "execution_count": 3, "id": "2e192496", "metadata": {}, "outputs": [], "source": [ "\"\"\"获得所有机位的相机内外参\"\"\"\n", "def get_camera_params(camera_path: Path) -> ak.Array:\n", " camera_dataset: ak.Array = ak.from_parquet(camera_path / \"camera_params.parquet\")\n", " return camera_dataset\n", "\n", "\"\"\"获取所有机位的2d检测数据\"\"\"\n", "def get_camera_detect(\n", " detect_path: Path, camera_port: list[int], camera_dataset: ak.Array\n", ") -> dict[int, ak.Array]:\n", " keypoint_data: dict[int, ak.Array] = {}\n", " for element_port in ak.to_numpy(camera_dataset[\"port\"]):\n", " if element_port in camera_port:\n", " keypoint_data[int(element_port)] = ak.from_parquet(\n", " detect_path / f\"{element_port}.parquet\"\n", " )\n", " return keypoint_data" ] }, { "cell_type": "code", "execution_count": 4, "id": "6eee3591", "metadata": {}, "outputs": [], "source": [ "# 相机内外参路径\n", "CAMERA_PATH = Path(\"/home/admin/Documents/2025_4_17/camera_params\")\n", "# 所有机位的相机内外参\n", "AK_CAMERA_DATASET: ak.Array = get_camera_params(CAMERA_PATH)\n", "# 2d检测数据路径\n", "DATASET_PATH = Path(\"/home/admin/Documents/2025_4_17/detect_result/many_people_01/\")\n", "# 指定机位的2d检测数据\n", "camera_port = [5607, 5608, 5609]\n", "KEYPOINT_DATASET = get_camera_detect(DATASET_PATH, camera_port, AK_CAMERA_DATASET)\n" ] }, { "cell_type": "code", "execution_count": 5, "id": "ce225126", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/tmp/ipykernel_2333927/1636344639.py:1: DeprecationWarning: __array__ implementation doesn't accept a copy keyword, so passing copy=False failed. __array__ must implement 'dtype' and 'copy' keyword arguments. To learn more, see the migration guide https://numpy.org/devdocs/numpy_2_0_migration_guide.html#adapting-to-changes-in-the-copy-keyword\n", " kps_5607 =np.array(KEYPOINT_DATASET[5607]['kps'])\n" ] }, { "data": { "text/plain": [ "(710, 2, 133, 2)" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stderr", "output_type": "stream", "text": [ "/tmp/ipykernel_2333927/1636344639.py:3: DeprecationWarning: __array__ implementation doesn't accept a copy keyword, so passing copy=False failed. __array__ must implement 'dtype' and 'copy' keyword arguments. To learn more, see the migration guide https://numpy.org/devdocs/numpy_2_0_migration_guide.html#adapting-to-changes-in-the-copy-keyword\n", " kps_5607_socers = np.array(KEYPOINT_DATASET[5607]['kps_scores'])\n" ] }, { "data": { "text/plain": [ "(710, 2, 133)" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "kps_5607 =np.array(KEYPOINT_DATASET[5607]['kps'])\n", "display(kps_5607.shape)\n", "kps_5607_socers = np.array(KEYPOINT_DATASET[5607]['kps_scores'])\n", "display(kps_5607_socers.shape)" ] }, { "cell_type": "code", "execution_count": 6, "id": "abf50aa8", "metadata": {}, "outputs": [], "source": [ "\n", "\"\"\"将所有2d检测数据打包\"\"\"\n", "\n", "\n", "@jaxtyped(typechecker=beartype)\n", "def undistort_points(\n", " points: Num[NDArray, \"M 2\"],\n", " camera_matrix: Num[NDArray, \"3 3\"],\n", " dist_coeffs: Num[NDArray, \"N\"],\n", ") -> Num[NDArray, \"M 2\"]:\n", " K = camera_matrix\n", " dist = dist_coeffs\n", " res = undistortPoints(points, K, dist, P=K) # type: ignore\n", " return res.reshape(-1, 2)\n", "\n", "\n", "@jaxtyped(typechecker=beartype)\n", "def to_transformation_matrix(\n", " rvec: Num[NDArray, \"3\"], tvec: Num[NDArray, \"3\"]\n", ") -> Num[NDArray, \"4 4\"]:\n", " res = np.eye(4)\n", " res[:3, :3] = R.from_rotvec(rvec).as_matrix()\n", " res[:3, 3] = tvec\n", " return res\n", "\n", "\n", "def from_camera_params(camera: ExternalCameraParams) -> Camera:\n", " rt = jnp.array(\n", " to_transformation_matrix(\n", " ak.to_numpy(camera[\"extrinsic\"][\"rvec\"]),\n", " ak.to_numpy(camera[\"extrinsic\"][\"tvec\"]),\n", " )\n", " )\n", " K = jnp.array(camera[\"intrinsic\"][\"camera_matrix\"]).reshape(3, 3)\n", " dist_coeffs = jnp.array(camera[\"intrinsic\"][\"distortion_coefficients\"])\n", " image_size = jnp.array(\n", " (camera[\"resolution\"][\"width\"], camera[\"resolution\"][\"height\"])\n", " )\n", " return Camera(\n", " id=camera[\"name\"],\n", " params=CameraParams(\n", " K=K,\n", " Rt=rt,\n", " dist_coeffs=dist_coeffs,\n", " image_size=image_size,\n", " ),\n", " )\n", "\n", "\n", "def preprocess_keypoint_dataset(\n", " dataset: Sequence[KeypointDataset],\n", " camera: Camera,\n", " fps: float,\n", " start_timestamp: datetime,\n", ") -> Generator[Detection, None, None]:\n", " frame_interval_s = 1 / fps\n", " for el in dataset:\n", " frame_index = el[\"frame_index\"]\n", " timestamp = start_timestamp + timedelta(seconds=frame_index * frame_interval_s)\n", " for kp, kp_score, boxes in zip(el[\"kps\"], el[\"kps_scores\"], el[\"boxes\"]):\n", " kp = undistort_points(\n", " np.asarray(kp),\n", " np.asarray(camera.params.K),\n", " np.asarray(camera.params.dist_coeffs),\n", " )\n", "\n", " yield Detection(\n", " keypoints=jnp.array(kp),\n", " confidences=jnp.array(kp_score),\n", " camera=camera,\n", " timestamp=timestamp,\n", " )\n", "\n", "\n", "def sync_batch_gen(\n", " gens: list[DetectionGenerator], diff: timedelta\n", ") -> Generator[list[Detection], Any, None]:\n", " from more_itertools import partition\n", "\n", " \"\"\"\n", " given a list of detection generators, return a generator that yields a batch of detections\n", "\n", " Args:\n", " gens: list of detection generators\n", " diff: maximum timestamp difference between detections to consider them part of the same batch\n", " \"\"\"\n", " N = len(gens)\n", " last_batch_timestamp: Optional[datetime] = None\n", " current_batch: list[Detection] = []\n", " paused: list[bool] = [False] * N\n", " finished: list[bool] = [False] * N\n", " unmached_detections: list[Detection] = []\n", "\n", " def reset_paused():\n", " \"\"\"\n", " reset paused list based on finished list\n", " \"\"\"\n", " for i in range(N):\n", " if not finished[i]:\n", " paused[i] = False\n", " else:\n", " paused[i] = True\n", "\n", " EPS = 1e-6\n", " # a small epsilon to avoid floating point precision issues\n", " diff_esp = diff - timedelta(seconds=EPS)\n", " while True:\n", " for i, gen in enumerate(gens):\n", " try:\n", " if finished[i] or paused[i]:\n", " if all(finished):\n", " if len(current_batch) > 0:\n", " # All generators exhausted, flush remaining batch and exit\n", " yield current_batch\n", " return\n", " else:\n", " continue\n", " val = next(gen)\n", " if last_batch_timestamp is None:\n", " last_batch_timestamp = val.timestamp\n", " current_batch.append(val)\n", " else:\n", " if abs(val.timestamp - last_batch_timestamp) >= diff_esp:\n", " unmached_detections.append(val)\n", " paused[i] = True\n", " if all(paused):\n", " yield current_batch\n", " reset_paused()\n", " last_batch_timestamp = last_batch_timestamp + diff\n", " bad, good = partition(\n", " lambda x: x.timestamp < unwrap(last_batch_timestamp),\n", " unmached_detections,\n", " )\n", " current_batch = list(good)\n", " unmached_detections = list(bad)\n", " else:\n", " current_batch.append(val)\n", " except StopIteration:\n", " return\n", "\n", "\n", "def get_batch_detect(\n", " keypoint_dataset,\n", " camera_dataset,\n", " camera_port: list[int],\n", " FPS: int = 24,\n", " batch_fps: int = 10,\n", ") -> Generator[list[Detection], Any, None]:\n", " gen_data = [\n", " preprocess_keypoint_dataset(\n", " keypoint_dataset[port],\n", " from_camera_params(camera_dataset[camera_dataset[\"port\"] == port][0]),\n", " FPS,\n", " datetime(2024, 4, 2, 12, 0, 0),\n", " )\n", " for port in camera_port\n", " ]\n", "\n", " sync_gen: Generator[list[Detection], Any, None] = sync_batch_gen(\n", " gen_data,\n", " timedelta(seconds=1 / batch_fps),\n", " )\n", " return sync_gen\n" ] }, { "cell_type": "code", "execution_count": 16, "id": "82ac4c3e", "metadata": {}, "outputs": [], "source": [ "# 将所有的2d检测数据打包\n", "sync_gen: Generator[list[Detection], Any, None] = get_batch_detect(\n", " KEYPOINT_DATASET,\n", " AK_CAMERA_DATASET,\n", " camera_port,\n", " batch_fps=24,\n", ")" ] }, { "cell_type": "code", "execution_count": 86, "id": "33559b73", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[Detection(, 2024-04-02 12:00:00.333333),\n", " Detection(, 2024-04-02 12:00:00.333333),\n", " Detection(, 2024-04-02 12:00:00.333333),\n", " Detection(, 2024-04-02 12:00:00.375000),\n", " Detection(, 2024-04-02 12:00:00.375000),\n", " Detection(, 2024-04-02 12:00:00.375000)]" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "6" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "detections = next(sync_gen)\n", "display(detections)\n", "display(len(detections))" ] }, { "cell_type": "code", "execution_count": 87, "id": "87d44153", "metadata": {}, "outputs": [], "source": [ "# 极限约束\n", "from app.camera import calculate_affinity_matrix_by_epipolar_constraint\n", "\n", "sorted_detections, affinity_matrix = calculate_affinity_matrix_by_epipolar_constraint(\n", " detections, alpha_2d=3500\n", ")" ] }, { "cell_type": "code", "execution_count": 88, "id": "821ca702", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[{'timestamp': '2024-04-02 12:00:00.333333',\n", " 'camera': 'AE_01',\n", " 'keypoint': (133, 2)},\n", " {'timestamp': '2024-04-02 12:00:00.375000',\n", " 'camera': 'AE_01',\n", " 'keypoint': (133, 2)},\n", " {'timestamp': '2024-04-02 12:00:00.333333',\n", " 'camera': 'AE_1A',\n", " 'keypoint': (133, 2)},\n", " {'timestamp': '2024-04-02 12:00:00.375000',\n", " 'camera': 'AE_1A',\n", " 'keypoint': (133, 2)},\n", " {'timestamp': '2024-04-02 12:00:00.333333',\n", " 'camera': 'AE_08',\n", " 'keypoint': (133, 2)},\n", " {'timestamp': '2024-04-02 12:00:00.375000',\n", " 'camera': 'AE_08',\n", " 'keypoint': (133, 2)}]" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "Array([[ -inf, -inf, 0.729, 0.63 , 0.549, 0.453],\n", " [ -inf, -inf, 0.786, 0.702, 0.651, 0.559],\n", " [0.729, 0.786, -inf, -inf, 0.846, 0.787],\n", " [0.63 , 0.702, -inf, -inf, 0.907, 0.847],\n", " [0.549, 0.651, 0.846, 0.907, -inf, -inf],\n", " [0.453, 0.559, 0.787, 0.847, -inf, -inf]], dtype=float32)" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "display(\n", " list(\n", " map(\n", " lambda x: {\n", " \"timestamp\": str(x.timestamp),\n", " \"camera\": x.camera.id,\n", " \"keypoint\": x.keypoints.shape,\n", " },\n", " sorted_detections,\n", " )\n", " )\n", ")\n", "with jnp.printoptions(precision=3, suppress=True):\n", " display(affinity_matrix)" ] }, { "cell_type": "code", "execution_count": 89, "id": "10499f36", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[[0, 2, 4], [1, 3, 5]]" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "array([[0, 0, 1, 0, 1, 0],\n", " [0, 0, 0, 1, 0, 1],\n", " [1, 0, 0, 0, 1, 0],\n", " [0, 1, 0, 0, 0, 1],\n", " [1, 0, 1, 0, 0, 0],\n", " [0, 1, 0, 1, 0, 0]])" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from app.solver._old import GLPKSolver\n", "\n", "def clusters_to_detections(\n", " clusters: list[list[int]], sorted_detections: list[Detection]\n", ") -> list[list[Detection]]:\n", " \"\"\"\n", " given a list of clusters (which is the indices of the detections in the sorted_detections list),\n", " extract the detections from the sorted_detections list\n", "\n", " Args:\n", " clusters: list of clusters, each cluster is a list of indices of the detections in the `sorted_detections` list\n", " sorted_detections: list of SORTED detections\n", "\n", " Returns:\n", " list of clusters, each cluster is a list of detections\n", " \"\"\"\n", " return [[sorted_detections[i] for i in cluster] for cluster in clusters]\n", "\n", "\n", "solver = GLPKSolver()\n", "aff_np = np.asarray(affinity_matrix).astype(np.float64)\n", "clusters, sol_matrix = solver.solve(aff_np)\n", "display(clusters)\n", "display(sol_matrix)" ] }, { "cell_type": "code", "execution_count": 90, "id": "b7a05c6b", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "image/png": "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3buT+dZERERkAEjqrajZs2dTUVGRsG3atGnMnj2bOXPmAFBeXk5TUxMbNmzgvPPOA2DlypVEo1EmT54cr/OjH/2IUCiE0+kEYPny5YwaNeqot6EA3G43brc7mW9HREREBprujjZubm42GzduNBs3bjSAefDBB83GjRvNrl27jlr/8FlRxhgzffp0M378eLN27VqzZs0aM3LkSDNz5sz4/qamJuPz+czs2bPNli1bzDPPPGMyMjLMY4891uV2alaUioqKiorKwC09nRXV7WCzatWqozbgxhtvPGr9owWb+vp6M3PmTJOVlWU8Ho+ZM2eOaW5uTqizefNmM2XKFON2u83gwYPNokWLutVOBRsVFRUVFZWBW3oabCxjjCEFBQIBvF5vXzdDREREesDv9+PxeLp9XMo+KypF85qIiMjnQk//Hk/ZYFNfX9/XTRAREZEeam5u7tFxSV+gr7/Iy8sDoKqqSrek+kggEKC0tJTdu3f3qDtRTp6uQd/TNehb+vz7XnevgTGG5ubmoz5rsitSNtjYbLHOKK/Xq/+Z+5jH49E16GO6Bn1P16Bv6fPve925BifTIZGyt6JERETk80fBRkRERFJGygYbt9vNfffdp9WI+5CuQd/TNeh7ugZ9S59/3zvV1yBl17ERERGRz5+U7bERERGRzx8FGxEREUkZCjYiIiKSMhRsREREJGWkZLB56KGHGD58OGlpaUyePJm33nqrr5uUMu6//34sy0ooo0ePju/v6Ohg7ty55Ofnk5WVxbXXXkttbW3COaqqqpgxYwYZGRkUFhYyb948wuHwqX4rA8bq1au5/PLLKSkpwbIsXnjhhYT9xhjuvfdeiouLSU9Pp6Kigh07diTUaWhoYNasWXg8HnJycrjppptoaWlJqPPOO+9w8cUXk5aWRmlpKYsXL+7ttzZgnOgafPOb3zzi92L69OkJdXQNem7hwoVMmjSJ7OxsCgsLueqqq9i+fXtCnWR997z66qtMmDABt9vN6aefzhNPPNHbb29A6Mo1+OIXv3jE78Gtt96aUOeUXIMePRO8H3vmmWeMy+Uy//f//l+zdetWc/PNN5ucnBxTW1vb101LCffdd585++yzTXV1dbzs378/vv/WW281paWlZsWKFWb9+vXmggsuMBdeeGF8fzgcNmPGjDEVFRVm48aN5sUXXzQFBQVmwYIFffF2BoQX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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "WIDTH = 2560\n", "HEIGHT = 1440\n", "\n", "clusters_detections = clusters_to_detections(clusters, sorted_detections)\n", "im = np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8)\n", "for el in clusters_detections[0]:\n", " im = visualize_whole_body(np.asarray(el.keypoints), im)\n", "\n", "p = plt.imshow(im)\n", "display(p)" ] }, { "cell_type": "code", "execution_count": 91, "id": "eac843c2", "metadata": {}, "outputs": [], "source": [ "# im_prime = np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8)\n", "# for el in clusters_detections[1]:\n", "# im_prime = visualize_whole_body(np.asarray(el.keypoints), im_prime)\n", "\n", "# p_prime = plt.imshow(im_prime)\n", "# display(p_prime)" ] }, { "cell_type": "code", "execution_count": 92, "id": "037dcc22", "metadata": {}, "outputs": [], "source": [ "@jaxtyped(typechecker=beartype)\n", "def triangulate_one_point_from_multiple_views_linear(\n", " proj_matrices: Float[Array, \"N 3 4\"],\n", " points: Num[Array, \"N 2\"],\n", " confidences: Optional[Float[Array, \"N\"]] = None,\n", " conf_threshold: float = 0.4, # 0.2\n", ") -> Float[Array, \"3\"]:\n", " \"\"\"\n", " Args:\n", " proj_matrices: 形状为(N, 3, 4)的投影矩阵序列\n", " points: 形状为(N, 2)的点坐标序列\n", " confidences: 形状为(N,)的置信度序列,范围[0.0, 1.0]\n", " conf_threshold: 置信度阈值,低于该值的观测不参与DLT\n", " Returns:\n", " point_3d: 形状为(3,)的三角测量得到的3D点\n", " \"\"\"\n", " assert len(proj_matrices) == len(points)\n", " N = len(proj_matrices)\n", " # 置信度加权DLT\n", " if confidences is None:\n", " weights = jnp.ones(N, dtype=jnp.float32)\n", " else:\n", " valid_mask = confidences >= conf_threshold\n", " weights = jnp.where(valid_mask, confidences, 0.0)\n", " sum_weights = jnp.sum(weights)\n", " weights = jnp.where(sum_weights > 0, weights / sum_weights, weights)\n", "\n", " A = jnp.zeros((N * 2, 4), dtype=jnp.float32)\n", " for i in range(N):\n", " x, y = points[i]\n", " row1 = proj_matrices[i, 2] * x - proj_matrices[i, 0]\n", " row2 = proj_matrices[i, 2] * y - proj_matrices[i, 1]\n", " A = A.at[2 * i].set(row1 * weights[i])\n", " A = A.at[2 * i + 1].set(row2 * weights[i])\n", "\n", " _, _, vh = jnp.linalg.svd(A, full_matrices=False)\n", " point_3d_homo = vh[-1]\n", " point_3d_homo = jnp.where(point_3d_homo[3] < 0, -point_3d_homo, point_3d_homo)\n", " is_zero_weight = jnp.sum(weights) == 0\n", " point_3d = jnp.where(\n", " is_zero_weight,\n", " jnp.full((3,), jnp.nan, dtype=jnp.float32),\n", " jnp.where(\n", " jnp.abs(point_3d_homo[3]) > 1e-8,\n", " point_3d_homo[:3] / point_3d_homo[3],\n", " jnp.full((3,), jnp.nan, dtype=jnp.float32),\n", " ),\n", " )\n", " return point_3d\n", "\n", "\n", "jaxtyped(typechecker=beartype)\n", "def triangulate_points_from_multiple_views_linear(\n", " proj_matrices: Float[Array, \"N 3 4\"],\n", " points: Num[Array, \"N P 2\"],\n", " confidences: Optional[Float[Array, \"N P\"]] = None,\n", ") -> Float[Array, \"P 3\"]:\n", " \"\"\"\n", " Batch‐triangulate P points observed by N cameras, linearly via SVD.\n", "\n", " Args:\n", " proj_matrices: (N, 3, 4) projection matrices\n", " points: (N, P, 2) image-coordinates per view\n", " confidences: (N, P, 1) optional per-view confidences in [0,1]\n", "\n", " Returns:\n", " (P, 3) 3D point for each of the P tracks\n", " \"\"\"\n", " N, P, _ = points.shape\n", " assert proj_matrices.shape[0] == N\n", "\n", " if confidences is None:\n", " conf = jnp.ones((N, P), dtype=jnp.float32)\n", " else:\n", " conf = jnp.array(confidences)\n", "\n", " vmap_triangulate = jax.vmap(\n", " triangulate_one_point_from_multiple_views_linear,\n", " in_axes=(None, 1, 1),\n", " out_axes=0,\n", " )\n", " return vmap_triangulate(proj_matrices, points, conf)\n", "\n" ] }, { "cell_type": "code", "execution_count": 93, "id": "8fc0074d", "metadata": {}, "outputs": [], "source": [ "def triangle_from_cluster(cluster: list[Detection]) -> Float[Array, \"3\"]:\n", " proj_matrices = jnp.array([el.camera.params.projection_matrix for el in cluster])\n", " points = jnp.array([el.keypoints for el in cluster])\n", " confidences = jnp.array([el.confidences for el in cluster])\n", " return triangulate_points_from_multiple_views_linear(\n", " proj_matrices, points, confidences=confidences\n", " )\n", "\n", "\n", "res = {\n", " \"a\": [triangle_from_cluster(clusters_detections[0]).tolist()],\n", " \"b\": [triangle_from_cluster(clusters_detections[1]).tolist()],\n", "}\n", "with open(\"samples/Test_YEU.json\", \"wb\") as f:\n", " f.write(orjson.dumps(res))" ] } ], "metadata": { "kernelspec": { "display_name": "cvth3pe", "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.12.9" } }, "nbformat": 4, "nbformat_minor": 5 }