From 3cc93e5eae34ffd86d352e77f32f08a45d7bfb35 Mon Sep 17 00:00:00 2001 From: crosstyan Date: Wed, 16 Apr 2025 18:53:05 +0800 Subject: [PATCH] feat: Enhance play notebook with new data structures and visualization utilities - Added new TypedDict classes for camera parameters, including Resolution, Intrinsic, and Extrinsic. - Updated dataset reading logic to accommodate new camera parameters structure. - Introduced functions for reading datasets by port and visualizing whole body keypoints. - Improved the affinity matrix calculation logic in the camera module. - Updated dependencies in pyproject.toml to include Plotly and SciPy for enhanced functionality. --- .vscode/settings.json | 5 +- app/camera/__init__.py | 6 +- app/solver/__init__.py | 4 +- app/solver/_old.py | 221 ++++++++++++ app/visualize/whole_body.py | 680 ++++++++++++++++++++++++++++++++++++ play.ipynb | 508 +++++++++++++++++++++++---- pyproject.toml | 4 +- uv.lock | 46 ++- 8 files changed, 1394 insertions(+), 80 deletions(-) create mode 100644 app/solver/_old.py create mode 100644 app/visualize/whole_body.py diff --git a/.vscode/settings.json b/.vscode/settings.json index dcb1530..af2c612 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,7 @@ { "python.analysis.typeCheckingMode": "basic", - "python.analysis.autoImportCompletions": true + "python.analysis.autoImportCompletions": true, + "cSpell.words": [ + "triu" + ] } \ No newline at end of file diff --git a/app/camera/__init__.py b/app/camera/__init__.py index 66717fa..0924610 100644 --- a/app/camera/__init__.py +++ b/app/camera/__init__.py @@ -350,7 +350,7 @@ def calculate_affinity_matrix_by_epipolar_constraint( else: camera_wise_split = classify_by_camera(detections) num_entries = sum(len(entries) for entries in camera_wise_split.values()) - affinity_matrix = jnp.zeros((num_entries, num_entries), dtype=jnp.float32) + affinity_matrix = jnp.ones((num_entries, num_entries), dtype=jnp.float32) * -jnp.inf affinity_matrix_mask = jnp.zeros((num_entries, num_entries), dtype=jnp.bool_) acc = 0 @@ -361,7 +361,7 @@ def calculate_affinity_matrix_by_epipolar_constraint( sorted_detections: list[Detection] = [] for camera_id, entries in camera_wise_split.items(): for i, _ in enumerate(entries): - camera_id_index_map[camera_id].add(acc + i) + camera_id_index_map[camera_id].add(acc) sorted_detections.append(entries[i]) acc += 1 camera_id_index_map_inverse[camera_id] = ( @@ -374,6 +374,8 @@ def calculate_affinity_matrix_by_epipolar_constraint( for i, det in enumerate(sorted_detections): other_indices = camera_id_index_map_inverse[det.camera.id] for j in other_indices: + if i == j: + continue if affinity_matrix_mask[i, j] or affinity_matrix_mask[j, i]: continue a = compute_affinity_epipolar_constraint_with_pairs( diff --git a/app/solver/__init__.py b/app/solver/__init__.py index c40d95c..0099a33 100644 --- a/app/solver/__init__.py +++ b/app/solver/__init__.py @@ -151,7 +151,7 @@ class _BIPSolver: clusters[int(label)].append(i) return list(clusters.values()) - def solve(self, affinity_matrix: NDArray, rtn_matrix=False): + def solve(self, affinity_matrix: NDArray): n_nodes = affinity_matrix.shape[0] if n_nodes <= 1: solution_x, sol_matrix = ( @@ -197,8 +197,6 @@ class _BIPSolver: sol_matrix += sol_matrix.T clusters = self.solution_mat_clusters(sol_matrix) - if not rtn_matrix: - return clusters return clusters, sol_matrix diff --git a/app/solver/_old.py b/app/solver/_old.py new file mode 100644 index 0000000..0919b4e --- /dev/null +++ b/app/solver/_old.py @@ -0,0 +1,221 @@ +import itertools +from collections import defaultdict + +import numpy as np +from cvxopt import glpk, matrix, spmatrix # type:ignore + +from jaxtyping import jaxtyped, Num + +from app._typing import NDArray + +glpk.options = {"msg_lev": "GLP_MSG_ERR"} + + +FROZEN_POS_EDGE = -1 +FROZEN_NEG_EDGE = -2 +INVALID_EDGE = -100 + + +class _BIPSolver: + """ + Binary Integer Programming solver + """ + + min_affinity: float + max_affinity: float + + def __init__(self, min_affinity: float = -np.inf, max_affinity: float = np.inf): + self.min_affinity = min_affinity + self.max_affinity = max_affinity + + @staticmethod + def _create_bip( + affinity_matrix: Num[NDArray, "N N"], min_affinity: float, max_affinity: float + ): + n_nodes = affinity_matrix.shape[0] + + # mask for selecting pairs of nodes + triu_mask = np.triu(np.ones_like(affinity_matrix, dtype=bool), 1) + + affinities = affinity_matrix[triu_mask] + frozen_pos_mask = affinities >= max_affinity + frozen_neg_mask = affinities <= min_affinity + unfrozen_mask = np.logical_not(frozen_pos_mask | frozen_neg_mask) + + # generate objective coefficients + objective_coefficients = affinities[unfrozen_mask] + + if len(objective_coefficients) == 0: # nio unfrozen edges + + objective_coefficients = np.asarray([affinity_matrix[0, -1]]) + unfrozen_mask = np.zeros_like(unfrozen_mask, dtype=np.bool) + unfrozen_mask[affinity_matrix.shape[1] - 1] = 1 + + # create matrix whose rows are the indices of the three edges in a + # constraint x_ij + x_ik - x_jk <= 1 + constraints_edges_idx = [] + if n_nodes >= 3: + edges_idx = np.empty_like(affinities, dtype=int) + edges_idx[frozen_pos_mask] = FROZEN_POS_EDGE + edges_idx[frozen_neg_mask] = FROZEN_NEG_EDGE + edges_idx[unfrozen_mask] = np.arange(len(objective_coefficients)) + nodes_to_edge_matrix = np.empty_like(affinity_matrix, dtype=int) + nodes_to_edge_matrix.fill(INVALID_EDGE) + nodes_to_edge_matrix[triu_mask] = edges_idx + + triplets = np.asarray( + tuple(itertools.combinations(range(n_nodes), 3)), dtype=int + ) + constraints_edges_idx = np.zeros_like(triplets) + constraints_edges_idx[:, 0] = nodes_to_edge_matrix[ + (triplets[:, 0], triplets[:, 1]) + ] + constraints_edges_idx[:, 1] = nodes_to_edge_matrix[ + (triplets[:, 0], triplets[:, 2]) + ] + constraints_edges_idx[:, 2] = nodes_to_edge_matrix[ + (triplets[:, 1], triplets[:, 2]) + ] + constraints_edges_idx = constraints_edges_idx[ + np.any(constraints_edges_idx >= 0, axis=1) + ] + + if len(constraints_edges_idx) == 0: # no constraints + constraints_edges_idx = np.asarray([0, 0, 0], dtype=int).reshape(-1, 3) + + # add remaining constraints by permutation + constraints_edges_idx = np.vstack( + ( + constraints_edges_idx, + np.roll(constraints_edges_idx, 1, axis=1), + np.roll(constraints_edges_idx, 2, axis=1), + ) + ) + + # clean redundant constraints + # x1 + x2 <= 2 + constraints_edges_idx = constraints_edges_idx[ + constraints_edges_idx[:, 2] != FROZEN_POS_EDGE + ] + # x1 - x2 <= 1 + constraints_edges_idx = constraints_edges_idx[ + np.all(constraints_edges_idx[:, 0:2] != FROZEN_NEG_EDGE, axis=1) + ] + if len(constraints_edges_idx) == 0: # no constraints + constraints_edges_idx = np.asarray([0, 0, 0], dtype=int).reshape(-1, 3) + + # generate constraint coefficients + constraints_coefficients = np.ones_like(constraints_edges_idx) + constraints_coefficients[:, 2] = -1 + + # generate constraint upper bounds + upper_bounds = np.ones(len(constraints_coefficients), dtype=float) + upper_bounds -= np.sum( + constraints_coefficients * (constraints_edges_idx == FROZEN_POS_EDGE), + axis=1, + ) + + # flatten constraints data into sparse matrix format + constraints_idx = np.repeat(np.arange(len(constraints_edges_idx)), 3) + constraints_edges_idx = constraints_edges_idx.reshape(-1) + constraints_coefficients = constraints_coefficients.reshape(-1) + + unfrozen_edges = constraints_edges_idx >= 0 + constraints_idx = constraints_idx[unfrozen_edges] + constraints_edges_idx = constraints_edges_idx[unfrozen_edges] + constraints_coefficients = constraints_coefficients[unfrozen_edges] + + return ( + objective_coefficients, + unfrozen_mask, + frozen_pos_mask, + frozen_neg_mask, + (constraints_coefficients, constraints_idx, constraints_edges_idx), + upper_bounds, + ) + + @staticmethod + def _solve_bip(objective_coefficients, sparse_constraints, upper_bounds): + raise NotImplementedError + + @staticmethod + def solution_mat_clusters(solution_mat: NDArray) -> list[list[int]]: + n = solution_mat.shape[0] + labels = np.arange(1, n + 1) + for i in range(n): + for j in range(i + 1, n): + if solution_mat[i, j] > 0: + labels[j] = labels[i] + + clusters = defaultdict(list) + for i, label in enumerate(labels): + clusters[label].append(i) + return list(clusters.values()) + + def solve(self, affinity_matrix: Num[NDArray, "N N"]): + n_nodes = affinity_matrix.shape[0] + if n_nodes <= 1: + solution_x, sol_matrix = ( + np.asarray([], dtype=int), + np.asarray([0] * n_nodes, dtype=int), + ) + sol_matrix = sol_matrix[:, None] + elif n_nodes == 2: + solution_matrix = np.zeros_like(affinity_matrix, dtype=int) + solution_matrix[0, 1] = affinity_matrix[0, 1] > 0 + solution_matrix += solution_matrix.T + solution_x = ( + [solution_matrix[0, 1]] + if self.min_affinity < affinity_matrix[0, 1] < self.max_affinity + else [] + ) + solution_x, sol_matrix = np.asarray(solution_x), solution_matrix + else: + # create BIP problem + ( + objective_coefficients, + unfrozen_mask, + frozen_pos_mask, + frozen_neg_mask, + sparse_constraints, + upper_bounds, + ) = self._create_bip(affinity_matrix, self.min_affinity, self.max_affinity) + + # solve + solution_x = self._solve_bip( + objective_coefficients, sparse_constraints, upper_bounds + ) + + # solution to matrix + all_sols = np.zeros_like(unfrozen_mask, dtype=int) + all_sols[unfrozen_mask] = np.array(solution_x, dtype=int).reshape(-1) + all_sols[frozen_neg_mask] = 0 + all_sols[frozen_pos_mask] = 1 + sol_matrix = np.zeros_like(affinity_matrix, dtype=int) + sol_matrix[np.triu(np.ones([n_nodes, n_nodes], dtype=int), 1) > 0] = ( + all_sols + ) + sol_matrix += sol_matrix.T + + clusters = self.solution_mat_clusters(sol_matrix) + return clusters, sol_matrix + + +class GLPKSolver(_BIPSolver): + def __init__(self, min_affinity=-np.inf, max_affinity=np.inf): + super().__init__(min_affinity, max_affinity) + + @staticmethod + def _solve_bip(objective_coefficients, sparse_constraints, upper_bounds): + c = matrix(-objective_coefficients) # max -> min + G = spmatrix( + *sparse_constraints, size=(len(upper_bounds), len(objective_coefficients)) + ) # G * x <= h + # G = spmatrix(sparse_constraints[0],sparse_constraints[1],sparse_constraints[2]) # G * x <= h + h = matrix(upper_bounds, tc="d") + + status, solution = glpk.ilp(c, G, h, B=set(range(len(c)))) + + assert solution is not None, "Solver error: {}".format(status) + + return np.asarray(solution, int).reshape(-1) diff --git a/app/visualize/whole_body.py b/app/visualize/whole_body.py new file mode 100644 index 0000000..5c74cc7 --- /dev/null +++ b/app/visualize/whole_body.py @@ -0,0 +1,680 @@ +from dataclasses import dataclass, field +from typing import ( + Any, + Dict, + Iterable, + List, + Literal, + Optional, + Sequence, + Tuple, + TypedDict, + cast, +) + +import cv2 +import matplotlib.pyplot as plt +import numpy as np +from beartype import beartype +from cv2.typing import MatLike +from jaxtyping import Float, Int, Num, jaxtyped + +from app._typing import NDArray + +# https://www.researchgate.net/figure/Whole-body-keypoints-as-defined-in-the-COCO-WholeBody-Dataset_fig3_358873962 +# https://github.com/jin-s13/COCO-WholeBody/blob/master/imgs/Fig2_anno.png +# body landmarks 1-17 +# foot landmarks 18-23 (18-20 right, 21-23 left) +# face landmarks 24-91 +# 24 start, counterclockwise to 40 as chin +# 41-45 right eyebrow, 46-50 left eyebrow +# https://www.neiltanna.com/face/rhinoplasty/nasal-analysis/ +# 51-54 nose (vertical), 55-59 nose (horizontal) +# 60-65 right eye, 66-71 left eye +# 72-83 outer lips (contour, counterclockwise) +# ... +# hand landmarks 92-133 (92-112 right, 113-133 left) + + +Color = Tuple[int, int, int] +COLOR_SPINE = (138, 201, 38) # green, spine & head +COLOR_ARMS = (255, 202, 58) # yellow, arms & shoulders +COLOR_LEGS = (25, 130, 196) # blue, legs & hips +COLOR_FINGERS = (255, 0, 0) # red, fingers +COLOR_FACE = (255, 200, 0) # yellow, face +COLOR_FOOT = (255, 128, 0) # orange, foot +COLOR_HEAD = (255, 0, 255) # purple, head + + +@dataclass +class Landmark: + index: int + """ + Note the index is 1-based, corresponding to the COCO WholeBody dataset. + + https://github.com/jin-s13/COCO-WholeBody/blob/master/imgs/Fig2_anno.png + """ + name: str + color: Color + + def __post_init__(self): + if self.index < 1: + raise ValueError(f"Index must be positive, got {self.index}") + + @property + def index_base_0(self) -> int: + """ + Returns the 0-based index of the landmark. + Useful for indexing into lists or arrays. + """ + return self.index - 1 + + +body_landmarks: dict[int, Landmark] = { + 0: Landmark(index=1, name="nose", color=COLOR_SPINE), + 1: Landmark(index=2, name="left_eye", color=COLOR_SPINE), + 2: Landmark(index=3, name="right_eye", color=COLOR_SPINE), + 3: Landmark(index=4, name="left_ear", color=COLOR_SPINE), + 4: Landmark(index=5, name="right_ear", color=COLOR_SPINE), + 5: Landmark(index=6, name="left_shoulder", color=COLOR_ARMS), + 6: Landmark(index=7, name="right_shoulder", color=COLOR_ARMS), + 7: Landmark(index=8, name="left_elbow", color=COLOR_ARMS), + 8: Landmark(index=9, name="right_elbow", color=COLOR_ARMS), + 9: Landmark(index=10, name="left_wrist", color=COLOR_ARMS), + 10: Landmark(index=11, name="right_wrist", color=COLOR_ARMS), + 11: Landmark(index=12, name="left_hip", color=COLOR_LEGS), + 12: Landmark(index=13, name="right_hip", color=COLOR_LEGS), + 13: Landmark(index=14, name="left_knee", color=COLOR_LEGS), + 14: Landmark(index=15, name="right_knee", color=COLOR_LEGS), + 15: Landmark(index=16, name="left_ankle", color=COLOR_LEGS), + 16: Landmark(index=17, name="right_ankle", color=COLOR_LEGS), +} + +foot_landmarks: dict[int, Landmark] = { + 17: Landmark(index=18, name="left_big_toe", color=COLOR_FOOT), + 18: Landmark(index=19, name="left_small_toe", color=COLOR_FOOT), + 19: Landmark(index=20, name="left_heel", color=COLOR_FOOT), + 20: Landmark(index=21, name="right_big_toe", color=COLOR_FOOT), + 21: Landmark(index=22, name="right_small_toe", color=COLOR_FOOT), + 22: Landmark(index=23, name="right_heel", color=COLOR_FOOT), +} + +face_landmarks: dict[int, Landmark] = { + # Chin contour (24-40) + 23: Landmark(index=24, name="chin_0", color=COLOR_FACE), + 24: Landmark(index=25, name="chin_1", color=COLOR_FACE), + 25: Landmark(index=26, name="chin_2", color=COLOR_FACE), + 26: Landmark(index=27, name="chin_3", color=COLOR_FACE), + 27: Landmark(index=28, name="chin_4", color=COLOR_FACE), + 28: Landmark(index=29, name="chin_5", color=COLOR_FACE), + 29: Landmark(index=30, name="chin_6", color=COLOR_FACE), + 30: Landmark(index=31, name="chin_7", color=COLOR_FACE), + 31: Landmark(index=32, name="chin_8", color=COLOR_FACE), + 32: Landmark(index=33, name="chin_9", color=COLOR_FACE), + 33: Landmark(index=34, name="chin_10", color=COLOR_FACE), + 34: Landmark(index=35, name="chin_11", color=COLOR_FACE), + 35: Landmark(index=36, name="chin_12", color=COLOR_FACE), + 36: Landmark(index=37, name="chin_13", color=COLOR_FACE), + 37: Landmark(index=38, name="chin_14", color=COLOR_FACE), + 38: Landmark(index=39, name="chin_15", color=COLOR_FACE), + 39: Landmark(index=40, name="chin_16", color=COLOR_FACE), + # Right eyebrow (41-45) + 40: Landmark(index=41, name="right_eyebrow_0", color=COLOR_FACE), + 41: Landmark(index=42, name="right_eyebrow_1", color=COLOR_FACE), + 42: Landmark(index=43, name="right_eyebrow_2", color=COLOR_FACE), + 43: Landmark(index=44, name="right_eyebrow_3", color=COLOR_FACE), + 44: Landmark(index=45, name="right_eyebrow_4", color=COLOR_FACE), + # Left eyebrow (46-50) + 45: Landmark(index=46, name="left_eyebrow_0", color=COLOR_FACE), + 46: Landmark(index=47, name="left_eyebrow_1", color=COLOR_FACE), + 47: Landmark(index=48, name="left_eyebrow_2", color=COLOR_FACE), + 48: Landmark(index=49, name="left_eyebrow_3", color=COLOR_FACE), + 49: Landmark(index=50, name="left_eyebrow_4", color=COLOR_FACE), + # Nasal Bridge (51-54) + 50: Landmark(index=51, name="nasal_bridge_0", color=COLOR_FACE), + 51: Landmark(index=52, name="nasal_bridge_1", color=COLOR_FACE), + 52: Landmark(index=53, name="nasal_bridge_2", color=COLOR_FACE), + 53: Landmark(index=54, name="nasal_bridge_3", color=COLOR_FACE), + # Nasal Base (55-59) + 54: Landmark(index=55, name="nasal_base_0", color=COLOR_FACE), + 55: Landmark(index=56, name="nasal_base_1", color=COLOR_FACE), + 56: Landmark(index=57, name="nasal_base_2", color=COLOR_FACE), + 57: Landmark(index=58, name="nasal_base_3", color=COLOR_FACE), + 58: Landmark(index=59, name="nasal_base_4", color=COLOR_FACE), + # Right eye (60-65) + 59: Landmark(index=60, name="right_eye_0", color=COLOR_FACE), + 60: Landmark(index=61, name="right_eye_1", color=COLOR_FACE), + 61: Landmark(index=62, name="right_eye_2", color=COLOR_FACE), + 62: Landmark(index=63, name="right_eye_3", color=COLOR_FACE), + 63: Landmark(index=64, name="right_eye_4", color=COLOR_FACE), + 64: Landmark(index=65, name="right_eye_5", color=COLOR_FACE), + # Left eye (66-71) + 65: Landmark(index=66, name="left_eye_0", color=COLOR_FACE), + 66: Landmark(index=67, name="left_eye_1", color=COLOR_FACE), + 67: Landmark(index=68, name="left_eye_2", color=COLOR_FACE), + 68: Landmark(index=69, name="left_eye_3", color=COLOR_FACE), + 69: Landmark(index=70, name="left_eye_4", color=COLOR_FACE), + 70: Landmark(index=71, name="left_eye_5", color=COLOR_FACE), + # lips (72-91) + 71: Landmark(index=72, name="lip_0", color=COLOR_FACE), + 72: Landmark(index=73, name="lip_1", color=COLOR_FACE), + 73: Landmark(index=74, name="lip_2", color=COLOR_FACE), + 74: Landmark(index=75, name="lip_3", color=COLOR_FACE), + 75: Landmark(index=76, name="lip_4", color=COLOR_FACE), + 76: Landmark(index=77, name="lip_5", color=COLOR_FACE), + 77: Landmark(index=78, name="lip_6", color=COLOR_FACE), + 78: Landmark(index=79, name="lip_7", color=COLOR_FACE), + 79: Landmark(index=80, name="lip_8", color=COLOR_FACE), + 80: Landmark(index=81, name="lip_9", color=COLOR_FACE), + 81: Landmark(index=82, name="lip_0", color=COLOR_FACE), + 82: Landmark(index=83, name="lip_1", color=COLOR_FACE), + 83: Landmark(index=84, name="lip_2", color=COLOR_FACE), + 84: Landmark(index=85, name="lip_3", color=COLOR_FACE), + 85: Landmark(index=86, name="lip_4", color=COLOR_FACE), + 86: Landmark(index=87, name="lip_5", color=COLOR_FACE), + 87: Landmark(index=88, name="lip_6", color=COLOR_FACE), + 88: Landmark(index=89, name="lip_7", color=COLOR_FACE), + 89: Landmark(index=90, name="lip_8", color=COLOR_FACE), + 90: Landmark(index=91, name="lip_9", color=COLOR_FACE), +} + +hand_landmarks: dict[int, Landmark] = { + # Right hand (92-112) + 91: Landmark(index=92, name="right_wrist", color=COLOR_FINGERS), # wrist/carpus + 92: Landmark( + index=93, name="right_thumb_metacarpal", color=COLOR_FINGERS + ), # thumb metacarpal + 93: Landmark( + index=94, name="right_thumb_mcp", color=COLOR_FINGERS + ), # metacarpophalangeal joint + 94: Landmark( + index=95, name="right_thumb_ip", color=COLOR_FINGERS + ), # interphalangeal joint + 95: Landmark(index=96, name="right_thumb_tip", color=COLOR_FINGERS), # tip of thumb + 96: Landmark( + index=97, name="right_index_metacarpal", color=COLOR_FINGERS + ), # index metacarpal + 97: Landmark( + index=98, name="right_index_mcp", color=COLOR_FINGERS + ), # metacarpophalangeal joint + 98: Landmark( + index=99, name="right_index_pip", color=COLOR_FINGERS + ), # proximal interphalangeal joint + 99: Landmark( + index=100, name="right_index_tip", color=COLOR_FINGERS + ), # tip of index + 100: Landmark( + index=101, name="right_middle_metacarpal", color=COLOR_FINGERS + ), # middle metacarpal + 101: Landmark( + index=102, name="right_middle_mcp", color=COLOR_FINGERS + ), # metacarpophalangeal joint + 102: Landmark( + index=103, name="right_middle_pip", color=COLOR_FINGERS + ), # proximal interphalangeal joint + 103: Landmark( + index=104, name="right_middle_tip", color=COLOR_FINGERS + ), # tip of middle + 104: Landmark( + index=105, name="right_ring_metacarpal", color=COLOR_FINGERS + ), # ring metacarpal + 105: Landmark( + index=106, name="right_ring_mcp", color=COLOR_FINGERS + ), # metacarpophalangeal joint + 106: Landmark( + index=107, name="right_ring_pip", color=COLOR_FINGERS + ), # proximal interphalangeal joint + 107: Landmark(index=108, name="right_ring_tip", color=COLOR_FINGERS), # tip of ring + 108: Landmark( + index=109, name="right_pinky_metacarpal", color=COLOR_FINGERS + ), # pinky metacarpal + 109: Landmark( + index=110, name="right_pinky_mcp", color=COLOR_FINGERS + ), # metacarpophalangeal joint + 110: Landmark( + index=111, name="right_pinky_pip", color=COLOR_FINGERS + ), # proximal interphalangeal joint + 111: Landmark( + index=112, name="right_pinky_tip", color=COLOR_FINGERS + ), # tip of pinky + # Left hand (113-133) + 112: Landmark(index=113, name="left_wrist", color=COLOR_FINGERS), # wrist/carpus + 113: Landmark( + index=114, name="left_thumb_metacarpal", color=COLOR_FINGERS + ), # thumb metacarpal + 114: Landmark( + index=115, name="left_thumb_mcp", color=COLOR_FINGERS + ), # metacarpophalangeal joint + 115: Landmark( + index=116, name="left_thumb_ip", color=COLOR_FINGERS + ), # interphalangeal joint + 116: Landmark( + index=117, name="left_thumb_tip", color=COLOR_FINGERS + ), # tip of thumb + 117: Landmark( + index=118, name="left_index_metacarpal", color=COLOR_FINGERS + ), # index metacarpal + 118: Landmark( + index=119, name="left_index_mcp", color=COLOR_FINGERS + ), # metacarpophalangeal joint + 119: Landmark( + index=120, name="left_index_pip", color=COLOR_FINGERS + ), # proximal interphalangeal joint + 120: Landmark( + index=121, name="left_index_tip", color=COLOR_FINGERS + ), # tip of index + 121: Landmark( + index=122, name="left_middle_metacarpal", color=COLOR_FINGERS + ), # middle metacarpal + 122: Landmark( + index=123, name="left_middle_mcp", color=COLOR_FINGERS + ), # metacarpophalangeal joint + 123: Landmark( + index=124, name="left_middle_pip", color=COLOR_FINGERS + ), # proximal interphalangeal joint + 124: Landmark( + index=125, name="left_middle_tip", color=COLOR_FINGERS + ), # tip of middle + 125: Landmark( + index=126, name="left_ring_metacarpal", color=COLOR_FINGERS + ), # ring metacarpal + 126: Landmark( + index=127, name="left_ring_mcp", color=COLOR_FINGERS + ), # metacarpophalangeal joint + 127: Landmark( + index=128, name="left_ring_pip", color=COLOR_FINGERS + ), # proximal interphalangeal joint + 128: Landmark(index=129, name="left_ring_tip", color=COLOR_FINGERS), # tip of ring + 129: Landmark( + index=130, name="left_pinky_metacarpal", color=COLOR_FINGERS + ), # pinky metacarpal + 130: Landmark( + index=131, name="left_pinky_mcp", color=COLOR_FINGERS + ), # metacarpophalangeal joint + 131: Landmark( + index=132, name="left_pinky_pip", color=COLOR_FINGERS + ), # proximal interphalangeal joint + 132: Landmark( + index=133, name="left_pinky_tip", color=COLOR_FINGERS + ), # tip of pinky +} +""" +Key corrections made: + 1. Each finger has a metacarpal bone in the palm + 2. Used standard anatomical abbreviations: + - MCP: MetaCarpoPhalangeal joint + - PIP: Proximal InterPhalangeal joint + - IP: InterPhalangeal joint (for thumb) + 3. The thumb has a different structure: + - Only one interphalangeal joint (IP) + - Different metacarpal orientation + 4. Used "tip" instead of specific phalanx names for endpoints + 5. Removed redundant bone naming since landmarks represent joints/connections +This better reflects the actual skeletal and joint structure of human hands while maintaining compatibility with the COCO-WholeBody dataset's keypoint system. +""" + +skeleton_joints = { + **body_landmarks, + **foot_landmarks, + **face_landmarks, + **hand_landmarks, +} + + +@dataclass +class Bone: + start: Landmark + end: Landmark + name: str + color: Color + + @staticmethod + def from_landmarks( + landmarks: Iterable[Landmark], + start_idx: int, + end_idx: int, + name: str, + color: Color, + ) -> "Bone": + """ + Note that the start and end indices are 1-based, corresponding to the COCO WholeBody dataset. + """ + start = next(filter(lambda x: x.index == start_idx, landmarks)) + end = next(filter(lambda x: x.index == end_idx, landmarks)) + return Bone(start=start, end=end, name=name, color=color) + + +# Note it's 0-based +# (15, 13), (13, 11), (16, 14), (14, 12), (11, 12), # 腿部 +# (5, 11), (6, 12), (5, 6), # 臀部和躯干 +# (5, 7), (7, 9), (6, 8), (8, 10), # 手臂 +# (1, 2), (0, 1), (0, 2), (1, 3), (2, 4), # 头部 +# (15, 17), (15, 18), (15, 19), # 左脚 +# (16, 20), (16, 21), (16, 22), # 右脚 +body_bones: list[Bone] = [ + # legs + Bone.from_landmarks( + skeleton_joints.values(), 16, 14, "left_tibia", COLOR_LEGS + ), # tibia & fibula + Bone.from_landmarks(skeleton_joints.values(), 14, 12, "left_femur", COLOR_LEGS), + Bone.from_landmarks(skeleton_joints.values(), 17, 15, "right_tibia", COLOR_LEGS), + Bone.from_landmarks(skeleton_joints.values(), 15, 13, "right_femur", COLOR_LEGS), + Bone.from_landmarks(skeleton_joints.values(), 12, 13, "pelvis", COLOR_LEGS), + # torso + Bone.from_landmarks( + skeleton_joints.values(), 6, 12, "left_contour", COLOR_SPINE + ), # contour of rib cage & pelvis (parallel to spine) + Bone.from_landmarks(skeleton_joints.values(), 7, 13, "right_contour", COLOR_SPINE), + Bone.from_landmarks(skeleton_joints.values(), 6, 7, "clavicle", COLOR_SPINE), + # arms + Bone.from_landmarks( + skeleton_joints.values(), 6, 8, "left_humerus", COLOR_ARMS + ), # humerus + Bone.from_landmarks( + skeleton_joints.values(), 8, 10, "left_radius", COLOR_ARMS + ), # radius & ulna + Bone.from_landmarks(skeleton_joints.values(), 7, 9, "right_humerus", COLOR_ARMS), + Bone.from_landmarks(skeleton_joints.values(), 9, 11, "right_radius", COLOR_ARMS), + # head + Bone.from_landmarks(skeleton_joints.values(), 2, 3, "head", COLOR_HEAD), + Bone.from_landmarks(skeleton_joints.values(), 1, 2, "left_eye", COLOR_HEAD), + Bone.from_landmarks(skeleton_joints.values(), 1, 3, "right_eye", COLOR_HEAD), + Bone.from_landmarks(skeleton_joints.values(), 2, 4, "left_ear", COLOR_HEAD), + Bone.from_landmarks(skeleton_joints.values(), 3, 5, "right_ear", COLOR_HEAD), + # foot + Bone.from_landmarks(skeleton_joints.values(), 16, 18, "left_foot_toe", COLOR_FOOT), + Bone.from_landmarks( + skeleton_joints.values(), 16, 19, "left_foot_small_toe", COLOR_FOOT + ), + Bone.from_landmarks(skeleton_joints.values(), 16, 20, "left_foot_heel", COLOR_FOOT), + Bone.from_landmarks(skeleton_joints.values(), 17, 21, "right_foot_toe", COLOR_FOOT), + Bone.from_landmarks( + skeleton_joints.values(), 17, 22, "right_foot_small_toe", COLOR_FOOT + ), + Bone.from_landmarks( + skeleton_joints.values(), 17, 23, "right_foot_heel", COLOR_FOOT + ), +] + +# note it's 0-based +# (91, 92), (92, 93), (93, 94), (94, 95), # 左拇指 +# (91, 96), (96, 97), (97, 98), (98, 99), # 左食指 +# (91, 100), (100, 101), (101, 102), (102, 103), # 左中指 +# (91, 104), (104, 105), (105, 106), (106, 107), # 左无名指 +# (91, 108), (108, 109), (109, 110), (110, 111), # 左小指 +# (112, 113), (113, 114), (114, 115), (115, 116), # 右拇指 +# (112, 117), (117, 118), (118, 119), (119, 120), # 右食指 +# (112, 121), (121, 122), (122, 123), (123, 124), # 右中指 +# (112, 125), (125, 126), (126, 127), (127, 128), # 右无名指 +# (112, 129), (129, 130), (130, 131), (131, 132) # 右小指 +hand_bones: list[Bone] = [ + # Right Thumb (Pollex) + Bone.from_landmarks( + hand_landmarks.values(), 92, 93, "right_thumb_metacarpal", COLOR_FINGERS + ), # First metacarpal + Bone.from_landmarks( + hand_landmarks.values(), 93, 94, "right_thumb_proximal_phalanx", COLOR_FINGERS + ), + Bone.from_landmarks( + hand_landmarks.values(), 94, 95, "right_thumb_distal_phalanx", COLOR_FINGERS + ), + # Right Index (Digit II) + Bone.from_landmarks( + hand_landmarks.values(), 92, 97, "right_index_metacarpal", COLOR_FINGERS + ), # Second metacarpal + Bone.from_landmarks( + hand_landmarks.values(), 97, 98, "right_index_proximal_phalanx", COLOR_FINGERS + ), + Bone.from_landmarks( + hand_landmarks.values(), 98, 99, "right_index_middle_phalanx", COLOR_FINGERS + ), + Bone.from_landmarks( + hand_landmarks.values(), 99, 100, "right_index_distal_phalanx", COLOR_FINGERS + ), + # Right Middle (Digit III) + Bone.from_landmarks( + hand_landmarks.values(), 92, 101, "right_middle_metacarpal", COLOR_FINGERS + ), # Third metacarpal + Bone.from_landmarks( + hand_landmarks.values(), + 101, + 102, + "right_middle_proximal_phalanx", + COLOR_FINGERS, + ), + Bone.from_landmarks( + hand_landmarks.values(), 102, 103, "right_middle_middle_phalanx", COLOR_FINGERS + ), + Bone.from_landmarks( + hand_landmarks.values(), 103, 104, "right_middle_distal_phalanx", COLOR_FINGERS + ), + # Right Ring (Digit IV) + Bone.from_landmarks( + hand_landmarks.values(), 92, 105, "right_ring_metacarpal", COLOR_FINGERS + ), # Fourth metacarpal + Bone.from_landmarks( + hand_landmarks.values(), 105, 106, "right_ring_proximal_phalanx", COLOR_FINGERS + ), + Bone.from_landmarks( + hand_landmarks.values(), 106, 107, "right_ring_middle_phalanx", COLOR_FINGERS + ), + Bone.from_landmarks( + hand_landmarks.values(), 107, 108, "right_ring_distal_phalanx", COLOR_FINGERS + ), + # Right Pinky (Digit V) + Bone.from_landmarks( + hand_landmarks.values(), 92, 109, "right_pinky_metacarpal", COLOR_FINGERS + ), # Fifth metacarpal + Bone.from_landmarks( + hand_landmarks.values(), 109, 110, "right_pinky_proximal_phalanx", COLOR_FINGERS + ), + Bone.from_landmarks( + hand_landmarks.values(), 110, 111, "right_pinky_middle_phalanx", COLOR_FINGERS + ), + Bone.from_landmarks( + hand_landmarks.values(), 111, 112, "right_pinky_distal_phalanx", COLOR_FINGERS + ), + # Left Thumb (Pollex) + Bone.from_landmarks( + hand_landmarks.values(), 113, 114, "left_thumb_metacarpal", COLOR_FINGERS + ), + Bone.from_landmarks( + hand_landmarks.values(), 114, 115, "left_thumb_proximal_phalanx", COLOR_FINGERS + ), + Bone.from_landmarks( + hand_landmarks.values(), 115, 116, "left_thumb_distal_phalanx", COLOR_FINGERS + ), + # Left Index (Digit II) + Bone.from_landmarks( + hand_landmarks.values(), 113, 118, "left_index_metacarpal", COLOR_FINGERS + ), + Bone.from_landmarks( + hand_landmarks.values(), 118, 119, "left_index_proximal_phalanx", COLOR_FINGERS + ), + Bone.from_landmarks( + hand_landmarks.values(), 119, 120, "left_index_middle_phalanx", COLOR_FINGERS + ), + Bone.from_landmarks( + hand_landmarks.values(), 120, 121, "left_index_distal_phalanx", COLOR_FINGERS + ), + # Left Middle (Digit III) + Bone.from_landmarks( + hand_landmarks.values(), 113, 122, "left_middle_metacarpal", COLOR_FINGERS + ), + Bone.from_landmarks( + hand_landmarks.values(), 122, 123, "left_middle_proximal_phalanx", COLOR_FINGERS + ), + Bone.from_landmarks( + hand_landmarks.values(), 123, 124, "left_middle_middle_phalanx", COLOR_FINGERS + ), + Bone.from_landmarks( + hand_landmarks.values(), 124, 125, "left_middle_distal_phalanx", COLOR_FINGERS + ), + # Left Ring (Digit IV) + Bone.from_landmarks( + hand_landmarks.values(), 113, 126, "left_ring_metacarpal", COLOR_FINGERS + ), + Bone.from_landmarks( + hand_landmarks.values(), 126, 127, "left_ring_proximal_phalanx", COLOR_FINGERS + ), + Bone.from_landmarks( + hand_landmarks.values(), 127, 128, "left_ring_middle_phalanx", COLOR_FINGERS + ), + Bone.from_landmarks( + hand_landmarks.values(), 128, 129, "left_ring_distal_phalanx", COLOR_FINGERS + ), + # Left Pinky (Digit V) + Bone.from_landmarks( + hand_landmarks.values(), 113, 130, "left_pinky_metacarpal", COLOR_FINGERS + ), + Bone.from_landmarks( + hand_landmarks.values(), 130, 131, "left_pinky_proximal_phalanx", COLOR_FINGERS + ), + Bone.from_landmarks( + hand_landmarks.values(), 131, 132, "left_pinky_middle_phalanx", COLOR_FINGERS + ), + Bone.from_landmarks( + hand_landmarks.values(), 132, 133, "left_pinky_distal_phalanx", COLOR_FINGERS + ), +] +""" +Key points about the hand bone structure: +1. Each finger (except thumb) has: + - Connection to metacarpal + - Proximal phalanx + - Middle phalanx + - Distal phalanx +2. Thumb is unique with: + - Metacarpal + - Proximal phalanx + - Distal phalanx (no middle phalanx) +3. All fingers connect back to the wrist (index 92 for right hand, 113 for left hand) +4. The anatomical names include the proper terms for each digit (Pollex for thumb, Digits II-V for fingers) +""" + +total_bones = body_bones + hand_bones + + +@jaxtyped(typechecker=beartype) +def visualize_whole_body( + keypoints: Num[NDArray, "133 2"], + frame: MatLike, + # keyword arguements + # kwargs + landmark_size: int = 1, + bone_size: int = 2, + output: Optional[MatLike] = None, + confidences: Optional[Num[NDArray, "133 1"]] = None, + confidence_threshold: float = 0.1, +): + """ + Visualize the whole body keypoints on the given frame. + """ + if output is None: + output = frame.copy() + for bone in total_bones: + start = keypoints[bone.start.index_base_0] + end = keypoints[bone.end.index_base_0] + start = tuple(start.astype(int)) + end = tuple(end.astype(int)) + if ( + confidences is not None + and confidences[bone.start.index_base_0] < confidence_threshold + and confidences[bone.end.index_base_0] < confidence_threshold + ): + continue + cv2.line(output, start, end, bone.color, bone_size) + for landmark in skeleton_joints.values(): + point = keypoints[landmark.index_base_0] + point = tuple(point.astype(int)) + if ( + confidences is not None + and confidences[landmark.index_base_0] < confidence_threshold + ): + continue + cv2.circle(output, point, landmark_size, landmark.color, -1) + return output + + +@jaxtyped(typechecker=beartype) +def visualize_17_keypoints( + keypoints: Num[NDArray, "17 2"], + frame: MatLike, + output: Optional[MatLike] = None, + confidences: Optional[Num[NDArray, "17 1"]] = None, + confidence_threshold: float = 0.1, + landmark_size: int = 1, + bone_size: int = 2, +): + """ + Visualize the first 17 keypoints on the given frame. + """ + if output is None: + output = frame.copy() + for bone in total_bones[:17]: + start = keypoints[bone.start.index_base_0] + end = keypoints[bone.end.index_base_0] + start = tuple(start.astype(int)) + end = tuple(end.astype(int)) + if ( + confidences is not None + and confidences[bone.start.index_base_0] < confidence_threshold + and confidences[bone.end.index_base_0] < confidence_threshold + ): + continue + cv2.line(output, start, end, bone.color, bone_size) + for landmark in list(body_landmarks.values())[:17]: + point = keypoints[landmark.index_base_0] + point = tuple(point.astype(int)) + if ( + confidences is not None + and confidences[landmark.index_base_0] < confidence_threshold + ): + continue + cv2.circle(output, point, landmark_size, landmark.color, -1) + return output + + +@jaxtyped(typechecker=beartype) +def visualize_whole_body_many( + keypoints: Num[NDArray, "N 133 2"], + frame: MatLike, + landmark_size: int = 1, + bone_size: int = 2, + output: Optional[MatLike] = None, + confidences: Optional[Num[NDArray, "N 133 1"]] = None, + confidence_threshold: float = 0.1, +): + """ + Visualize a batch of whole body keypoints on the given frame. + """ + if len(keypoints) == 0: + return frame + if output is None: + output = frame.copy() + if confidences is None: + for keypoint in keypoints: + output = visualize_whole_body( + keypoint, + frame, + landmark_size, + bone_size, + output=output, + confidences=None, + ) + return output + if confidences is not None: + assert len(keypoints) == len( + confidences + ), f"Expected same length, got {len(keypoints)} and {len(confidences)}" + for keypoint, confidence in zip(keypoints, confidences): + output = visualize_whole_body( + keypoint, + frame, + landmark_size, + bone_size, + output=output, + confidences=confidence, + confidence_threshold=confidence_threshold, + ) + return output diff --git a/play.ipynb b/play.ipynb index 31c2e02..dd18282 100644 --- a/play.ipynb +++ b/play.ipynb @@ -2,79 +2,60 @@ "cells": [ { "cell_type": "code", - "execution_count": 17, + "execution_count": 34, "metadata": {}, "outputs": [], "source": [ + "from datetime import datetime, timedelta\n", "from pathlib import Path\n", + "from typing import Generator, Sequence, TypeAlias, TypedDict\n", + "\n", "import awkward as ak\n", - "import numpy as np\n", - "from matplotlib import pyplot as plt\n", "import jax\n", - "import jax.numpy as jnp" + "import jax.numpy as jnp\n", + "import numpy as np\n", + "from jaxtyping import Array, Num\n", + "from matplotlib import pyplot as plt\n", + "\n", + "from app.camera import Detection\n", + "from app.camera import Camera, CameraParams\n", + "\n", + "NDArray: TypeAlias = np.ndarray" ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 35, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "
{AE_01: {extrinsic: {rvec: [...], ...}, intrinsic: {...}, keypoints: ..., ...},\n",
-       " AE_1A: {extrinsic: {rvec: [...], ...}, intrinsic: {...}, keypoints: ..., ...},\n",
-       " AE_08: {extrinsic: {rvec: [...], ...}, intrinsic: {...}, keypoints: ..., ...}}\n",
-       "--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\n",
+       "
[{name: 'AE_01', port: 5602, intrinsic: {...}, extrinsic: {...}, ...},\n",
+       " {name: 'AE_1A', port: 5601, intrinsic: {...}, extrinsic: {...}, ...},\n",
+       " {name: 'AE_08', port: 5600, intrinsic: {...}, extrinsic: {...}, ...}]\n",
+       "------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\n",
        "backend: cpu\n",
-       "nbytes: 5.9 MB\n",
-       "type: {\n",
-       "    AE_01: {\n",
-       "        extrinsic: {\n",
-       "            rvec: var * float64,\n",
-       "            tvec: var * float64\n",
-       "        },\n",
-       "        intrinsic: {\n",
-       "            camera_matrix: var * var * float64,\n",
-       "            distortion_coefficients: var * float64\n",
-       "        },\n",
-       "        keypoints: var * var * var * float64,\n",
-       "        confidences: var * var * float64,\n",
-       "        matrix: var * var * float64,\n",
-       "        projection_matrix: var * var * float64\n",
+       "nbytes: 823 B\n",
+       "type: 3 * {\n",
+       "    name: string,\n",
+       "    port: int64,\n",
+       "    intrinsic: {\n",
+       "        camera_matrix: var * var * var * float64,\n",
+       "        distortion_coefficients: var * float64\n",
        "    },\n",
-       "    AE_1A: {\n",
-       "        extrinsic: {\n",
-       "            rvec: var * float64,\n",
-       "            tvec: var * float64\n",
-       "        },\n",
-       "        intrinsic: {\n",
-       "            camera_matrix: var * var * float64,\n",
-       "            distortion_coefficients: var * float64\n",
-       "        },\n",
-       "        keypoints: var * var * var * float64,\n",
-       "        confidences: var * var * float64,\n",
-       "        matrix: var * var * float64,\n",
-       "        projection_matrix: var * var * float64\n",
+       "    extrinsic: {\n",
+       "        rvec: var * float64,\n",
+       "        tvec: var * float64\n",
        "    },\n",
-       "    AE_08: {\n",
-       "        extrinsic: {\n",
-       "            rvec: var * float64,\n",
-       "            tvec: var * float64\n",
-       "        },\n",
-       "        intrinsic: {\n",
-       "            camera_matrix: var * var * float64,\n",
-       "            distortion_coefficients: var * float64\n",
-       "        },\n",
-       "        keypoints: var * var * var * float64,\n",
-       "        confidences: var * var * float64,\n",
-       "        matrix: var * var * float64,\n",
-       "        projection_matrix: var * var * float64\n",
+       "    resolution: {\n",
+       "        width: int64,\n",
+       "        height: int64\n",
        "    }\n",
        "}
" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -82,35 +63,436 @@ } ], "source": [ - "DATASET_PATH = Path(\"samples/camera_dataset.parquet\")\n", - "AK_CAMERA_DATASET: ak.Array = ak.from_parquet(DATASET_PATH)[0]\n", + "DATASET_PATH = Path(\"samples\") / \"04_02\" \n", + "AK_CAMERA_DATASET: ak.Array = ak.from_parquet(DATASET_PATH / \"camera_params.parquet\")\n", "display(AK_CAMERA_DATASET)" ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 36, "metadata": {}, "outputs": [], "source": [ - "from app.camera import Camera, CameraParams\n", + "class Resolution(TypedDict):\n", + " width: int\n", + " height: int\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", - "def preprocess(key:str, record: ak.Record) -> Camera:\n", - " K = jnp.array(ak.to_numpy(record[\"intrinsic\"][\"camera_matrix\"])) # type: ignore\n", - " Rt = jnp.array(ak.to_numpy(record[\"matrix\"]))\n", - " dist_coeffs = jnp.array(ak.to_numpy(record[\"intrinsic\"][\"distortion_coefficients\"])) # type: ignore\n", - " size = (2560, 1440)\n", - " return Camera(id=key, params=CameraParams(K=K, Rt=Rt, dist_coeffs=dist_coeffs), size=size)" + "class Extrinsic(TypedDict):\n", + " rvec: Num[NDArray, \"3\"]\n", + " tvec: Num[NDArray, \"3\"]\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": 20, + "execution_count": 37, "metadata": {}, "outputs": [], "source": [ - "CAMERA_DATASET = {key: preprocess(key, AK_CAMERA_DATASET[key]) for key in AK_CAMERA_DATASET.fields} # type: ignore" + "def read_dataset_by_port(port: int) -> ak.Array:\n", + " P = DATASET_PATH / f\"{port}.parquet\"\n", + " return ak.from_parquet(P)\n", + "\n", + "KEYPOINT_DATASET = {int(p): read_dataset_by_port(p) for p in ak.to_numpy(AK_CAMERA_DATASET[\"port\"])}" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
[{frame_index: 0, boxes: [[599, ...], [...]], kps: [...], kps_scores: ..., ...},\n",
+       " {frame_index: 1, boxes: [[599, ...], [...]], kps: [...], kps_scores: ..., ...},\n",
+       " {frame_index: 2, boxes: [[599, ...], [...]], kps: [...], kps_scores: ..., ...},\n",
+       " {frame_index: 3, boxes: [[599, ...], [...]], kps: [...], kps_scores: ..., ...},\n",
+       " {frame_index: 4, boxes: [[598, ...], [...]], kps: [...], kps_scores: ..., ...},\n",
+       " {frame_index: 5, boxes: [[596, ...], [...]], kps: [...], kps_scores: ..., ...},\n",
+       " {frame_index: 6, boxes: [[594, ...], [...]], kps: [...], kps_scores: ..., ...},\n",
+       " {frame_index: 7, boxes: [[595, ...], [...]], kps: [...], kps_scores: ..., ...},\n",
+       " {frame_index: 8, boxes: [[595, ...], [...]], kps: [...], kps_scores: ..., ...},\n",
+       " {frame_index: 9, boxes: [[595, ...], [...]], kps: [...], kps_scores: ..., ...},\n",
+       " ...,\n",
+       " {frame_index: 520, boxes: [[1.09e+03, ...], ...], kps: [...], ...},\n",
+       " {frame_index: 521, boxes: [[1.09e+03, ...], ...], kps: [...], ...},\n",
+       " {frame_index: 522, boxes: [[1.09e+03, ...], ...], kps: [...], ...},\n",
+       " {frame_index: 523, boxes: [[1.09e+03, ...], ...], kps: [...], ...},\n",
+       " {frame_index: 524, boxes: [[1.09e+03, ...], ...], kps: [...], ...},\n",
+       " {frame_index: 525, boxes: [[1.09e+03, ...], ...], kps: [...], ...},\n",
+       " {frame_index: 526, boxes: [[1.09e+03, ...], ...], kps: [...], ...},\n",
+       " {frame_index: 527, boxes: [[1.09e+03, ...], ...], kps: [...], ...},\n",
+       " {frame_index: 528, boxes: [[1.09e+03, ...], ...], kps: [...], ...}]\n",
+       "-----------------------------------------------------------------------------------------------------------------------------------------------\n",
+       "backend: cpu\n",
+       "nbytes: 4.6 MB\n",
+       "type: 529 * {\n",
+       "    frame_index: int64,\n",
+       "    boxes: var * var * float64,\n",
+       "    kps: var * var * var * float64,\n",
+       "    kps_scores: var * var * float64\n",
+       "}
" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "KEYPOINT_DATASET[5601]" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [], + "source": [ + "from scipy.spatial.transform import Rotation as R" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [], + "source": [ + "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", + "def to_transformation_matrix(rvec: Num[NDArray, \"3\"], tvec: Num[NDArray, \"3\"]) -> Num[NDArray, \"4 4\"]:\n", + " r = R.from_rotvec(rvec) # type: ignore\n", + " t = tvec.reshape(3, 1)\n", + " return np.concatenate([np.concatenate([r.as_matrix(), t], axis=1), np.array([[0, 0, 0, 1]])], axis=0)\n", + "\n", + "def from_camera_params(camera: ExternalCameraParams) -> Camera:\n", + " rt = jnp.array(to_transformation_matrix(ak.to_numpy(camera[\"extrinsic\"][\"rvec\"]), ak.to_numpy(camera[\"extrinsic\"][\"tvec\"])))\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((camera[\"resolution\"][\"width\"], camera[\"resolution\"][\"height\"]))\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", + "def preprocess_keypoint_dataset(dataset: Sequence[KeypointDataset], camera: Camera,fps: float, start_timestamp: datetime) -> 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 in zip(el[\"kps\"], el[\"kps_scores\"]):\n", + " yield Detection(\n", + " keypoints=jnp.array(kp),\n", + " confidences=jnp.array(kp_score),\n", + " camera=camera,\n", + " timestamp=timestamp,\n", + " )\n" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Optional\n", + "from copy import deepcopy\n", + "\n", + "DetectionGenerator: TypeAlias = Generator[Detection, None, None]\n", + "\n", + "\n", + "def sync_batch_gen(gens: list[DetectionGenerator], diff: timedelta):\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", + " next_batch_timestamp: Optional[datetime] = None\n", + " current_batch: list[Detection] = []\n", + " next_batch: list[Detection] = []\n", + " paused: list[bool] = [False] * N\n", + " finished: list[bool] = [False] * N\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", + " 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", + " next_batch.append(val)\n", + " if next_batch_timestamp is None:\n", + " next_batch_timestamp = val.timestamp\n", + " paused[i] = True\n", + " if all(paused):\n", + " yield current_batch\n", + " current_batch = next_batch\n", + " next_batch = []\n", + " last_batch_timestamp = next_batch_timestamp\n", + " next_batch_timestamp = None\n", + " reset_paused()\n", + " else:\n", + " current_batch.append(val)\n", + " except StopIteration:\n", + " finished[i] = True\n", + " paused[i] = True\n", + " if all(finished):\n", + " if len(current_batch) > 0:\n", + " # All generators exhausted, flush remaining batch and exit\n", + " yield current_batch\n", + " break" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.041666666666666664" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "FPS = 24\n", + "image_gen_5600 = preprocess_keypoint_dataset(KEYPOINT_DATASET[5600], from_camera_params(AK_CAMERA_DATASET[AK_CAMERA_DATASET[\"port\"] == 5600][0]), FPS, datetime(2024, 4, 2, 12, 0, 0)) # type: ignore\n", + "image_gen_5601 = preprocess_keypoint_dataset(KEYPOINT_DATASET[5601], from_camera_params(AK_CAMERA_DATASET[AK_CAMERA_DATASET[\"port\"] == 5601][0]), FPS, datetime(2024, 4, 2, 12, 0, 0)) # type: ignore\n", + "image_gen_5602 = preprocess_keypoint_dataset(KEYPOINT_DATASET[5602], from_camera_params(AK_CAMERA_DATASET[AK_CAMERA_DATASET[\"port\"] == 5602][0]), FPS, datetime(2024, 4, 2, 12, 0, 0)) # type: ignore\n", + "\n", + "display(1/FPS)\n", + "sync_gen = sync_batch_gen([image_gen_5600, image_gen_5601, image_gen_5602], timedelta(seconds=1/FPS))" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [], + "source": [ + "detections = next(sync_gen)" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [], + "source": [ + "from app.camera import calculate_affinity_matrix_by_epipolar_constraint\n", + "\n", + "sorted_detections, affinity_matrix = calculate_affinity_matrix_by_epipolar_constraint(detections, \n", + " alpha_2d=1800)" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'timestamp': '2024-04-02 12:00:00', 'camera': 'AE_08'},\n", + " {'timestamp': '2024-04-02 12:00:00', 'camera': 'AE_08'},\n", + " {'timestamp': '2024-04-02 12:00:00', 'camera': 'AE_1A'},\n", + " {'timestamp': '2024-04-02 12:00:00', 'camera': 'AE_1A'},\n", + " {'timestamp': '2024-04-02 12:00:00', 'camera': 'AE_01'},\n", + " {'timestamp': '2024-04-02 12:00:00', 'camera': 'AE_01'}]" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Array([[ -inf, -inf, 0.625, 0.321, -0.243, 0.018],\n", + " [ -inf, -inf, 0.9 , 0.795, 0.293, 0.568],\n", + " [ 0.625, 0.9 , -inf, -inf, 0.211, 0.371],\n", + " [ 0.321, 0.795, -inf, -inf, 0.684, 0.793],\n", + " [-0.243, 0.293, 0.211, 0.684, -inf, -inf],\n", + " [ 0.018, 0.568, 0.371, 0.793, -inf, -inf]], dtype=float32)" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "display(list(map(lambda x: {\"timestamp\": str(x.timestamp), \"camera\": x.camera.id}, sorted_detections)))\n", + "with jnp.printoptions(precision=3, suppress=True):\n", + " display(affinity_matrix)" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[[0, 2, 5], [1, 3, 4]]" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "array([[0, 0, 1, 0, 0, 1],\n", + " [0, 0, 0, 1, 1, 0],\n", + " [1, 0, 0, 0, 0, 1],\n", + " [0, 1, 0, 0, 1, 0],\n", + " [0, 1, 0, 1, 0, 0],\n", + " [1, 0, 1, 0, 0, 0]])" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from app.solver._old import GLPKSolver\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": 51, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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vd/vrXOgBQsWEA6HU6WhoSGTb0lEREROAhkPNntDzaZNm/jDH/5ASUlJ2v6amhra29tZuXJlatvSpUuxbZsZM2ak6ixfvpx4PJ6qs3jxYiZOnNjvYygAv99PIBBIKyIiIjLMDLS3cUdHh1m1apVZtWqVAcz9999vVq1aZbZu3WpisZj5yle+YkaPHm0++OADs3PnzlTZf4TTnDlzzNlnn23q6+vNW2+9ZSZMmGDmzp2b2t/e3m7Ky8vNddddZ9auXWuee+45k5eXZx5//PEjbqdGRamoqKioqJy8ZbCjogYcbN54441+G3D99debLVu2HLSBb7zxRuocLS0tZu7cuaagoMAEAgFzww03mI6OjrTXWb16tbn44ouN3+83o0aNMvfee++A2qlgo6KioqKicvKWwQYbyxhjyEKRSIRgMDjUzRAREZFBCIfDg+pWorWiREREJGso2IiIiEjWULARERGRrKFgIyIiIllDwUZERESyhoKNiIiIZA0FGxEREckaCjYiIiKSNRRsREREJGso2IiIiEjWULARERGRrKFgIyIiIllDwUZERESyhoKNiIiIZA0FGxEREckaCjYiIiKSNRRsREREJGso2IiIiEjWULARERGRrKFgIyIiIllDwUZERESyhoKNiIiIZA0FGxEREckaCjYiIiKSNRRsREREJGso2IiIiEjWULARERGRrKFgIyIiIlljwMFm+fLlXHHFFVRWVmJZFi+99NJB637nO9/BsiweeOCBtO2tra3MmzePQCBAYWEhN954I52dnWl11qxZwyWXXEJOTg5VVVUsWrRooE0VERGRYWbAwaarq4tp06bx8MMPH7Leiy++yDvvvENlZWWfffPmzWPdunUsXryYV155heXLl3PzzTen9kciEWbNmsXYsWNZuXIl9913H3fffTdPPPHEQJsrIiIiw4k5CoB58cUX+2z//PPPzahRo8zatWvN2LFjzb/+67+m9q1fv94AZsWKFaltr732mrEsy2zfvt0YY8wjjzxiioqKTDQaTdW54447zMSJE4+4beFw2AAqKioqKioqJ2EJh8MDDybGmIz3sbFtm+uuu47bb7+dM844o8/+uro6CgsLmT59empbbW0tLpeL+vr6VJ1LL70Un8+XqjN79mw2btxIW1tbppssIiIiWcKT6RP+8pe/xOPx8Ld/+7f97m9sbKSsrCy9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7z0Odw02b95s7rnnHvPee++ZLVu2mJdfftmMHz/eXHrppalzHK9rkHXBxhhjHnroITNmzBjj8/nM+eefb955552hblLW+NrXvmYqKiqMz+czo0aNMl/72tfM5s2bU/t7enrM9773PVNUVGTy8vLMV7/6VbNz5860c3z22WfmsssuM7m5uaa0tNT86Ec/MvF4/Hi/lZPGG2+8YYA+5frrrzfGOEO+77zzTlNeXm78fr+ZOXOm2bhxY9o5WlpazNy5c01BQYEJBALmhhtuMB0dHWl1Vq9ebS6++GLj9/vNqFGjzL333nu83uIJ71DXoLu728yaNcuMHDnSeL1eM3bsWHPTTTf1+Z8pXYPB6+9nD5hf//rXqTqZ+tvzxhtvmLPOOsv4fD4zfvz4tNcYzg53DbZt22YuvfRSU1xcbPx+vzn11FPN7bffnjaPjTHH5xpYexosIiIictLLqj42IiIiMrwp2IiIiEjWULARERGRrKFgIyIiIllDwUZERESyhoKNiIiIZA0FGxEREckaCjYiIiKSNRRsREREJGso2IiIiEjWULARERGRrKFgIyIiIlnj/wfaK4SwMBYPUgAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from app.visualize.whole_body import visualize_whole_body\n", + "from matplotlib import pyplot as plt\n", + "\n", + "WIDTH = 2560\n", + "HEIGHT = 1440\n", + "\n", + "im = np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8)\n", + "for i in clusters[0]:\n", + " el = sorted_detections[i]\n", + " im = visualize_whole_body(np.asarray(el.keypoints), im)\n", + "\n", + "p = plt.imshow(im)\n", + "display(p)" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "im_prime = np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8)\n", + "for i in clusters[1]:\n", + " el = sorted_detections[i]\n", + " im_prime = visualize_whole_body(np.asarray(el.keypoints), im_prime)\n", + "\n", + "p_prime= plt.imshow(im_prime)\n", + "display(p_prime)" ] } ], diff --git a/pyproject.toml b/pyproject.toml index 303143b..ce4ed63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,10 +12,12 @@ dependencies = [ "jax[cuda12]>=0.5.1", "jaxtyping>=0.2.38", "matplotlib>=3.10.1", - "opencv-contrib-python-headless>=4.11.0.86", + "opencv-python-headless>=4.11.0.86", "orjson>=3.10.15", "pandas>=2.2.3", + "plotly>=6.0.1", "pyarrow>=19.0.1", + "scipy>=1.15.2", "torch>=2.6.0", "torchvision>=0.21.0", "typeguard>=4.4.2", diff --git a/uv.lock b/uv.lock index 4e6ccdf..c6744d0 100644 --- a/uv.lock +++ b/uv.lock @@ -451,10 +451,12 @@ dependencies = [ { name = "jax", extra = ["cuda12"] }, { name = "jaxtyping" }, { name = "matplotlib" }, - { name = "opencv-contrib-python-headless" }, + { name = "opencv-python-headless" }, { name = "orjson" }, { name = "pandas" }, + { name = "plotly" }, { name = "pyarrow" }, + { name = "scipy" }, { name = "torch" }, { name = "torchvision" }, { name = "typeguard" }, @@ -474,10 +476,12 @@ requires-dist = [ { name = "jax", extras = ["cuda12"], specifier = ">=0.5.1" }, { name = "jaxtyping", specifier = ">=0.2.38" }, { name = "matplotlib", specifier = ">=3.10.1" }, - { name = "opencv-contrib-python-headless", specifier = ">=4.11.0.86" }, + { name = "opencv-python-headless", specifier = ">=4.11.0.86" }, { name = "orjson", specifier = ">=3.10.15" }, { name = "pandas", specifier = ">=2.2.3" }, + { name = "plotly", specifier = ">=6.0.1" }, { name = "pyarrow", specifier = ">=19.0.1" }, + { name = "scipy", specifier = ">=1.15.2" }, { name = "torch", specifier = ">=2.6.0" }, { name = "torchvision", specifier = ">=0.21.0" }, { name = "typeguard", specifier = ">=4.4.2" }, @@ -1501,6 +1505,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 }, ] +[[package]] +name = "narwhals" +version = "1.35.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/6a/a98fa5e9d530a428a0cd79d27f059ed65efd3a07aad61a8c93e323c9c20b/narwhals-1.35.0.tar.gz", hash = "sha256:07477d18487fbc940243b69818a177ed7119b737910a8a254fb67688b48a7c96", size = 265784 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/b3/5781eb874f04cb1e882a7d93cf30abcb00362a3205c5f3708a7434a1a2ac/narwhals-1.35.0-py3-none-any.whl", hash = "sha256:7562af132fa3f8aaaf34dc96d7ec95bdca29d1c795e8fcf14e01edf1d32122bc", size = 325708 }, +] + [[package]] name = "nbclient" version = "0.10.2" @@ -1808,20 +1821,20 @@ wheels = [ ] [[package]] -name = "opencv-contrib-python-headless" +name = "opencv-python-headless" version = "4.11.0.86" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/cc/295e9a4e783ca71ba1b8fbd34e51bc603eba4611afcfc7de1b09b2d6ed8d/opencv-contrib-python-headless-4.11.0.86.tar.gz", hash = "sha256:839319098a73264c580c97cb1ca835f7fce3d30e4fa9fa6d4d0618fff551be0b", size = 150579288 } +sdist = { url = "https://files.pythonhosted.org/packages/36/2f/5b2b3ba52c864848885ba988f24b7f105052f68da9ab0e693cc7c25b0b30/opencv-python-headless-4.11.0.86.tar.gz", hash = "sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798", size = 95177929 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/fd/501948c96381bc94f76dde8357c2d7cf2bb98e7734366856d7b32d5e6c31/opencv_contrib_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:b34485c0164057726eee8cb80d5cd5dedaab5edde716451fb4107dcc60cf40f0", size = 46276903 }, - { url = "https://files.pythonhosted.org/packages/29/fa/ba711201197bd0f9c9cda21f739393b9e14c2394b0d8ba886a877071a576/opencv_contrib_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:ec88044ecb426549b05a8a6187e68051299848c4556ef8cef310621ca8f4316b", size = 66524225 }, - { url = "https://files.pythonhosted.org/packages/83/ec/b3fb322e8bac7b797f98676c34599827920b3972e4d664bbdf8de84d7fca/opencv_contrib_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8dc2f4109904ffa55967bf9ceb1521ce46d66c333e2f6261dfa1f957a1dbde0", size = 35122073 }, - { url = "https://files.pythonhosted.org/packages/7a/80/26c4ad9459498fc9213dea7254c8d6cb7717b279306b070588a2781559d4/opencv_contrib_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e7a86bf02e8157a2d9fce26d44eaafd31113fc21fc8b4f44ff56c28ab32fdba", size = 56078660 }, - { url = "https://files.pythonhosted.org/packages/c0/38/2ce4259eca6ca356e3757b596d7d583b4ab0be4a482c9f4dacaa3eb688d1/opencv_contrib_python_headless-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:d2c10564c01f6c308ded345a3b37359714e694361e593e515c148465eda09c2a", size = 35092082 }, - { url = "https://files.pythonhosted.org/packages/49/c1/c7600136283a2d4d3327968bdd895ba917a033d5a5498b6c7ffcd78c772c/opencv_contrib_python_headless-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:2671a828e5c8ec9d237dd8506a9e0268487d37e07625725f1a6de5fa973ea7fa", size = 46095689 }, + { url = "https://files.pythonhosted.org/packages/dc/53/2c50afa0b1e05ecdb4603818e85f7d174e683d874ef63a6abe3ac92220c8/opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:48128188ade4a7e517237c8e1e11a9cdf5c282761473383e77beb875bb1e61ca", size = 37326460 }, + { url = "https://files.pythonhosted.org/packages/3b/43/68555327df94bb9b59a1fd645f63fafb0762515344d2046698762fc19d58/opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:a66c1b286a9de872c343ee7c3553b084244299714ebb50fbdcd76f07ebbe6c81", size = 56723330 }, + { url = "https://files.pythonhosted.org/packages/45/be/1438ce43ebe65317344a87e4b150865c5585f4c0db880a34cdae5ac46881/opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6efabcaa9df731f29e5ea9051776715b1bdd1845d7c9530065c7951d2a2899eb", size = 29487060 }, + { url = "https://files.pythonhosted.org/packages/dd/5c/c139a7876099916879609372bfa513b7f1257f7f1a908b0bdc1c2328241b/opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e0a27c19dd1f40ddff94976cfe43066fbbe9dfbb2ec1907d66c19caef42a57b", size = 49969856 }, + { url = "https://files.pythonhosted.org/packages/95/dd/ed1191c9dc91abcc9f752b499b7928aacabf10567bb2c2535944d848af18/opencv_python_headless-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:f447d8acbb0b6f2808da71fddd29c1cdd448d2bc98f72d9bb78a7a898fc9621b", size = 29324425 }, + { url = "https://files.pythonhosted.org/packages/86/8a/69176a64335aed183529207ba8bc3d329c2999d852b4f3818027203f50e6/opencv_python_headless-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:6c304df9caa7a6a5710b91709dd4786bf20a74d57672b3c31f7033cc638174ca", size = 39402386 }, ] [[package]] @@ -2065,6 +2078,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, ] +[[package]] +name = "plotly" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "narwhals" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/cc/e41b5f697ae403f0b50e47b7af2e36642a193085f553bf7cc1169362873a/plotly-6.0.1.tar.gz", hash = "sha256:dd8400229872b6e3c964b099be699f8d00c489a974f2cfccfad5e8240873366b", size = 8094643 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/65/ad2bc85f7377f5cfba5d4466d5474423a3fb7f6a97fd807c06f92dd3e721/plotly-6.0.1-py3-none-any.whl", hash = "sha256:4714db20fea57a435692c548a4eb4fae454f7daddf15f8d8ba7e1045681d7768", size = 14805757 }, +] + [[package]] name = "prometheus-client" version = "0.21.1"