From 5e0b591f076d84625fe9fefc58f04c9c93082a4f Mon Sep 17 00:00:00 2001 From: adhavan18 Date: Fri, 4 Sep 2026 18:07:17 +0530 Subject: [PATCH 1/3] docs(cookbooks): pose augmentation with left/right keypoint remapping Closes #2519. sv.KeyPoints.xy has a fixed row order per skeleton (row 1 is always left_eye, row 2 always right_eye, and so on), but a horizontal flip only mirrors coordinates. Albumentations' HorizontalFlip has no notion of which row is semantically left or right, so after mirroring, row 1 still says left_eye even though its position is now where the right eye visually is. The cookbook builds a small deterministic pose (right arm raised, no model or dataset needed), flips it, and shows the fix: after mirroring coordinates, also swap each left/right pair's row so the label matches the mirrored side. AlbumentationsX's KeypointParams(label_mapping=...) automates this same swap inside the transform for larger pipelines; doing it explicitly here (rather than adding a hard torch dependency just to demonstrate one kwarg) shows exactly what that option does under the hood, and keeps the cookbook as self-contained as the issue asked for. Includes a numeric verification (every remapped row lands exactly where albumentations' own pixel-index mirror, width - 1 - x, says it should) and a rendered comparison (left joints blue, right red) that makes the bug visible: the naive flip still colors the raised arm red after mirroring, the corrected version colors it blue. Executed end-to-end via jupyter nbconvert before committing; matches the repo's existing notebooks in shipping baked-in outputs (mkdocs-jupyter's execute: false renders them as-is). --- ...se-augmentation-left-right-remapping.ipynb | 312 ++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100644 docs/notebooks/pose-augmentation-left-right-remapping.ipynb diff --git a/docs/notebooks/pose-augmentation-left-right-remapping.ipynb b/docs/notebooks/pose-augmentation-left-right-remapping.ipynb new file mode 100644 index 0000000000..662f70ee18 --- /dev/null +++ b/docs/notebooks/pose-augmentation-left-right-remapping.ipynb @@ -0,0 +1,312 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "48a8a347", + "metadata": {}, + "source": [ + "# Pose augmentation with left/right keypoint remapping\n", + "\n", + "`sv.KeyPoints.xy` has a fixed row order: row 1 is always `left_eye`, row 2 is always `right_eye`, and so on for every left/right pair in the skeleton. A horizontal flip mirrors *coordinates*, but on its own it does nothing about *row order* — after mirroring, the position that used to hold the left eye is now where the right eye visually is, but row 1 is still labeled `left_eye`.\n", + "\n", + "This notebook builds a small, deterministic pose (no model or dataset needed), flips it with [Albumentations](https://albumentations.ai/)' `HorizontalFlip`, and shows the fix: after mirroring coordinates, also swap each left/right pair's row so the label matches the mirrored side. [AlbumentationsX](https://github.com/albumentations-team/AlbumentationsX)'s `KeypointParams(label_mapping=...)` automates exactly this swap inside the transform for larger pipelines — doing it explicitly here shows what that option is doing under the hood." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cadaa940", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-04T12:30:59.505110Z", + "iopub.status.busy": "2026-09-04T12:30:59.504112Z", + "iopub.status.idle": "2026-09-04T12:31:15.554442Z", + "shell.execute_reply": "2026-09-04T12:31:15.553424Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\sktam\\AppData\\Local\\Temp\\ccwork\\svenv\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], + "source": [ + "import albumentations as A\n", + "import cv2\n", + "import numpy as np\n", + "import supervision as sv" + ] + }, + { + "cell_type": "markdown", + "id": "8fa643d7", + "metadata": {}, + "source": [ + "## A deterministic pose\n", + "\n", + "17 points in COCO order — the order `sv.KeyPoints.from_ultralytics`, `from_inference`, and RF-DETR's pose models already return. The pose has one arm raised so a wrong flip is visibly different from a correct one, not just numerically." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "e73b9c50", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-04T12:31:15.561491Z", + "iopub.status.busy": "2026-09-04T12:31:15.560603Z", + "iopub.status.idle": "2026-09-04T12:31:15.579433Z", + "shell.execute_reply": "2026-09-04T12:31:15.576902Z" + } + }, + "outputs": [], + "source": [ + "COCO_KEYPOINT_NAMES = [\n", + " \"nose\", \"left_eye\", \"right_eye\", \"left_ear\", \"right_ear\",\n", + " \"left_shoulder\", \"right_shoulder\", \"left_elbow\", \"right_elbow\",\n", + " \"left_wrist\", \"right_wrist\", \"left_hip\", \"right_hip\",\n", + " \"left_knee\", \"right_knee\", \"left_ankle\", \"right_ankle\",\n", + "]\n", + "\n", + "# (left_index, right_index) for every mirrored pair.\n", + "LEFT_RIGHT_PAIRS = [\n", + " (1, 2), (3, 4), (5, 6), (7, 8), (9, 10), (11, 12), (13, 14), (15, 16),\n", + "]\n", + "\n", + "IMAGE_WIDTH, IMAGE_HEIGHT = 400, 400\n", + "\n", + "# right arm raised: an asymmetric pose so a naive flip is visibly wrong.\n", + "POSE = np.array([\n", + " [200, 60], # nose\n", + " [190, 50], [210, 50], # left_eye, right_eye\n", + " [180, 55], [220, 55], # left_ear, right_ear\n", + " [170, 120], [230, 120], # left_shoulder, right_shoulder\n", + " [160, 180], [260, 90], # left_elbow, right_elbow (raised)\n", + " [150, 230], [280, 50], # left_wrist, right_wrist (raised)\n", + " [180, 220], [220, 220], # left_hip, right_hip\n", + " [175, 300], [225, 300], # left_knee, right_knee\n", + " [170, 370], [230, 370], # left_ankle, right_ankle\n", + "], dtype=np.float32)\n", + "\n", + "key_points = sv.KeyPoints(xy=POSE[np.newaxis, :, :], class_id=np.array([0]))" + ] + }, + { + "cell_type": "markdown", + "id": "baa69f4f", + "metadata": {}, + "source": [ + "## Flip the coordinates\n", + "\n", + "`A.HorizontalFlip` mirrors each `(x, y)` pair. It has no idea which row is semantically left or right, so row order is untouched — this is the naive result the issue describes." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "69febf39", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-04T12:31:15.584447Z", + "iopub.status.busy": "2026-09-04T12:31:15.583450Z", + "iopub.status.idle": "2026-09-04T12:31:15.603397Z", + "shell.execute_reply": "2026-09-04T12:31:15.600818Z" + } + }, + "outputs": [], + "source": [ + "transform = A.Compose(\n", + " [A.HorizontalFlip(p=1.0)],\n", + " keypoint_params=A.KeypointParams(format=\"xy\", remove_invisible=False),\n", + ")\n", + "\n", + "blank_image = np.zeros((IMAGE_HEIGHT, IMAGE_WIDTH, 3), dtype=np.uint8)\n", + "mirrored = transform(image=blank_image, keypoints=key_points.xy[0].tolist())\n", + "mirrored_xy = np.array(mirrored[\"keypoints\"], dtype=np.float32)\n", + "\n", + "# Rebuilt straight from the mirrored coordinates: coordinates are correct,\n", + "# but row order still says row 1 is \"left_eye\" even though its mirrored\n", + "# position is now on the visual right of the frame.\n", + "naive_flip = sv.KeyPoints(xy=mirrored_xy[np.newaxis, :, :], class_id=np.array([0]))" + ] + }, + { + "cell_type": "markdown", + "id": "8f76c009", + "metadata": {}, + "source": [ + "## Fix: swap each pair's row after mirroring\n", + "\n", + "This is the step AlbumentationsX's `KeypointParams(label_mapping=...)` automates. Doing it by hand here: build the permutation once from `LEFT_RIGHT_PAIRS`, then rebuild `sv.KeyPoints` with that row order applied to the mirrored coordinates." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "603b1ee7", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-04T12:31:15.610300Z", + "iopub.status.busy": "2026-09-04T12:31:15.610300Z", + "iopub.status.idle": "2026-09-04T12:31:15.622547Z", + "shell.execute_reply": "2026-09-04T12:31:15.620884Z" + } + }, + "outputs": [], + "source": [ + "remap = list(range(len(COCO_KEYPOINT_NAMES)))\n", + "for left, right in LEFT_RIGHT_PAIRS:\n", + " remap[left], remap[right] = remap[right], remap[left]\n", + "\n", + "correct_flip = sv.KeyPoints(\n", + " xy=mirrored_xy[remap][np.newaxis, :, :], class_id=np.array([0])\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "90757710", + "metadata": {}, + "source": [ + "## Verify\n", + "\n", + "For every pair, the remapped left row should sit exactly where the *original* right row mirrors to, and vice versa. Albumentations mirrors pixel index `i` to `(width - 1 - i)`, so that's the formula to check against rather than a bare `width - x`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "0acf5055", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-04T12:31:15.627133Z", + "iopub.status.busy": "2026-09-04T12:31:15.627133Z", + "iopub.status.idle": "2026-09-04T12:31:15.640539Z", + "shell.execute_reply": "2026-09-04T12:31:15.638506Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "All left/right pairs land where they should.\n" + ] + } + ], + "source": [ + "MIRROR_X = IMAGE_WIDTH - 1\n", + "\n", + "for left, right in LEFT_RIGHT_PAIRS:\n", + " expected_left = [MIRROR_X - POSE[right][0], POSE[right][1]]\n", + " assert np.allclose(correct_flip.xy[0][left], expected_left), COCO_KEYPOINT_NAMES[left]\n", + "\n", + " expected_right = [MIRROR_X - POSE[left][0], POSE[left][1]]\n", + " assert np.allclose(correct_flip.xy[0][right], expected_right), COCO_KEYPOINT_NAMES[right]\n", + "\n", + "# And confirm the naive version actually is wrong for this asymmetric pose,\n", + "# not just differently-labeled-but-coincidentally-equal:\n", + "assert not np.allclose(naive_flip.xy[0][9], correct_flip.xy[0][9])\n", + "\n", + "print(\"All left/right pairs land where they should.\")" + ] + }, + { + "cell_type": "markdown", + "id": "93e3c80d", + "metadata": {}, + "source": [ + "## See the difference\n", + "\n", + "Left-side joints in blue, right-side in red. Watch the raised arm: in \"naive flip\" it's still colored red (\"right\") even though mirroring put it on the visual left — exactly the bug. In \"correct flip\" it's blue, matching its true anatomical side after the mirror." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "c676905e", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-04T12:31:15.645538Z", + "iopub.status.busy": "2026-09-04T12:31:15.644545Z", + "iopub.status.idle": "2026-09-04T12:31:23.179852Z", + "shell.execute_reply": "2026-09-04T12:31:23.178831Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA7AAAAFICAYAAABp4xKfAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvctoD+AAAAAlwSFlzAAAPYQAAD2EBqD+naQAAUmZJREFUeJzt3Qd0HNXZ//FntqvLKpYbCPeKwTYlmJgWWugJ9U1CKHkJ2IHQQgk1EEgjocdAAFMTXtofQguQEFOMTQm26QZckLskq5fd1Wp3/ue5thxjS6uunZG+n3N0vNauVtdjzU/3mVvGsm3bFgAAAAAAHM6T6gYAAAAAANARFLAAAAAAAFeggAUAAAAAuAIFLAAAAADAFShgAQAAAACuQAELAAAAAHAFClgAAAAAgCtQwAIAAAAAXIECFgAAAADgCr6OvrC4uLh3WwIAAAAAGJBKSko69DpGYAEAAAAArkABCwAAAABwBQpYAAAAAIArUMACAAAAAFyBAhYAAAAA0L92Ie4pfr9fPB6PRKPRNl+TlpYmTU1NEo/Hu/T+tm1Lc3NzN1sKAB3n8/nE6/UmzbaO0PfIzMyUQCAgDQ0NJgs10zTb9L31MQC4LR81u7bv11mWJaFQSLKysky+hcNhicVi5nP6J305AI4oYI899lgZMmSI3HXXXa0WqFq83nDDDfLQQw/JRx991On3P+WUU6Smpkaee+65HmoxALTvoIMOkl133VX+/Oc/SyQS6fIhO+644+T000+XFStWmCz74IMPZMOGDXLIIYfI3Llzpbq6mv8OAK6hF9++973vSVVVlbz22muSSCS2Pjds2DC5/PLLzZ/vvPOOee7xxx+Xc889V15++WV5/fXXU9p2AM7U51OIP/nkk60h1Rq94vbSSy/Jxo0bu/T+48aNk9GjR3ezlQDQOV999ZUsWLDAZFh3O3pPP/20XHXVVWbUQvNs7dq1piPXncIYAFJBZ5WMGTNGdt55ZzPiui3Nt9zcXLnyyivl73//u0ybNs3MQHnllVdk1apV/IcB6N0RWA2l/fff34wSKA2ft99+W8aPHy9Tp041HbH8/HzZtGmTuQqn04inTJkixx9/vHlORxkyMjLMFTcNNO0M/s///I/U1tbKhAkTzNW5hQsXyjPPPGOKXw28M844Q0aNGmWK3QceeEDWr1/fU/8cAAOYZplmzNChQ2Xs2LHy5ZdfysMPP2ymt+kskRNOOEH23HNPk09PPPGEmS2Sl5dnXv+d73xH0tPT5dlnnzXvVVRUJIceeqi8+uqrZorcSSedZHLx888/l6eeemrriKq+TkdeNe+0CNb3KCwsNJmp0+tGjBghn332mfz4xz+WiooK09HTHNXZJu+++y5TiwG0SnNCc0lniehFspaRTZ0FpzlyxBFHSE5OjsyfP998aN7o5xobG2XkyJGmP6ZLGrSvVlBQYPphxcXFpr+n7/H888/LkiVLTAbNmDHDfK3299566y3zvXRm3H777Wfeb6eddpL7779f1qxZY2bknXjiieZzZ511lixevHhrm7VvV1paKgceeKBpjxa/2jfULH7wwQdNFgMYuHpsBFYD6+KLL5YvvvjCBMwvf/lL2WeffUzo6Oe/9a1vyerVq00Y7r333ib8rrjiCtOhW7RokRxzzDGmINVO42GHHWamGWvgXXbZZVJfXy/Lli2T2bNny9FHH22+35lnnimDBg2Sxx57zATrb37zG/MnAHTX5MmT5ZprrpHs7GzTqTryyCPl7LPP3pp1WrzqNDe9eKbZo6/TEYZ9993XjJJqgaudMr2wpx1Efb3SqXKafU8++aS5KHfppZea0Qal612XLl1qimItiPWxfk4NHz7c5KG+VgtYvfCn7SovLzcZqRcJAaA1Rx11lJx//vmmH/Xxxx+bPpn2w6ZPn276YZo5ehFM+2C6hEEvxs2ZM8fkng4MaHGrX6N9Ou3HzZw50zz/5ptvmvfTHNOLcpp12verq6szz2lxqkWqft9169aZEVX9Pro0Qi1fvtz0GcvKyszndaZJCy2OtYjdbbfd5OqrrzbFs76nFuI///nP+Y8GBrgeGYHVwlE7aDri+uijj5qrcHqlTAtR/VxJSYn8+te/NlfTdJRBO2NayGpHr2XkVIvUCy+80HT49GphC51ufM8995jH+nUaujrNRDuAeiVRRzx0NEI7jhq6ANBdmkMrV66UW265xYyGaiZpx+62224znaj333/fZI8WmJo7Woy2fJ2OVmgHTtfDasdMRxBaRhb22msvuf32283X6ejEz372M9lll13M0grNQJ1lojNU9KKeZt93v/vdre/bMvVOv1bzT0dB9CKeLpvQUWItetngCcC2gsGgGRXVbHnkkUfM53RNvW6OpBfqdCRU+2GaPzqAcMABB5gs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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "LEFT_INDICES = {left for left, _ in LEFT_RIGHT_PAIRS}\n", + "RIGHT_INDICES = {right for _, right in LEFT_RIGHT_PAIRS}\n", + "\n", + "\n", + "def render(key_points: sv.KeyPoints, canvas: np.ndarray) -> np.ndarray:\n", + " scene = sv.EdgeAnnotator(color=sv.Color(200, 200, 200), thickness=2).annotate(\n", + " canvas.copy(), key_points\n", + " )\n", + "\n", + " num_points = key_points.xy.shape[1]\n", + " left_mask = np.array([[i in LEFT_INDICES for i in range(num_points)]])\n", + " right_mask = np.array([[i in RIGHT_INDICES for i in range(num_points)]])\n", + "\n", + " left_kp = sv.KeyPoints(xy=key_points.xy, class_id=key_points.class_id, visible=left_mask)\n", + " right_kp = sv.KeyPoints(xy=key_points.xy, class_id=key_points.class_id, visible=right_mask)\n", + "\n", + " scene = sv.VertexAnnotator(color=sv.Color(0, 100, 255), radius=6).annotate(scene, left_kp)\n", + " scene = sv.VertexAnnotator(color=sv.Color(255, 0, 0), radius=6).annotate(scene, right_kp)\n", + " return scene\n", + "\n", + "\n", + "panels = [\n", + " (\"original\", key_points),\n", + " (\"naive flip\", naive_flip),\n", + " (\"correct flip\", correct_flip),\n", + "]\n", + "canvas = np.full((IMAGE_HEIGHT, IMAGE_WIDTH, 3), 30, dtype=np.uint8)\n", + "images = [render(kp, canvas) for _, kp in panels]\n", + "combined = np.concatenate(images, axis=1)\n", + "for i, (title, _) in enumerate(panels):\n", + " cv2.putText(\n", + " combined, title, (i * IMAGE_WIDTH + 10, 20),\n", + " cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1, cv2.LINE_AA,\n", + " )\n", + "\n", + "sv.plot_image(combined, size=(12, 4))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 6b7f782e1e7b0c2ac1ab8dde18398763d10638af Mon Sep 17 00:00:00 2001 From: adhavan18 Date: Wed, 9 Sep 2026 21:28:41 +0530 Subject: [PATCH 2/3] address copilot review on the pose-flip cookbook - add the missing install cell (supervision, albumentations, opencv-python) and open in colab badge, per CONTRIBUTING.md's cookbook checklist - add the cookbooks.html card so it's discoverable from the cookbooks page - scope the "row 1 is always left_eye" and "these converters return coco-17" claims to coco-17 specifically, since keypoint order actually comes from the source model/skeleton - clear the committed stderr output that leaked a local windows path and an unrelated tqdm warning (fixed at the source by installing ipywidgets, not just stripped from the diff) - add a docstring to the render() helper per agents.md - re-executed end to end so every output is real, not hand-edited --- ...se-augmentation-left-right-remapping.ipynb | 117 ++++++++++++------ docs/theme/cookbooks.html | 4 + 2 files changed, 82 insertions(+), 39 deletions(-) diff --git a/docs/notebooks/pose-augmentation-left-right-remapping.ipynb b/docs/notebooks/pose-augmentation-left-right-remapping.ipynb index 662f70ee18..6fb18833c3 100644 --- a/docs/notebooks/pose-augmentation-left-right-remapping.ipynb +++ b/docs/notebooks/pose-augmentation-left-right-remapping.ipynb @@ -2,26 +2,46 @@ "cells": [ { "cell_type": "markdown", - "id": "48a8a347", + "id": "021ab594", "metadata": {}, "source": [ "# Pose augmentation with left/right keypoint remapping\n", "\n", - "`sv.KeyPoints.xy` has a fixed row order: row 1 is always `left_eye`, row 2 is always `right_eye`, and so on for every left/right pair in the skeleton. A horizontal flip mirrors *coordinates*, but on its own it does nothing about *row order* — after mirroring, the position that used to hold the left eye is now where the right eye visually is, but row 1 is still labeled `left_eye`.\n", + "[![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/roboflow/supervision/blob/develop/docs/notebooks/pose-augmentation-left-right-remapping.ipynb)\n", "\n", - "This notebook builds a small, deterministic pose (no model or dataset needed), flips it with [Albumentations](https://albumentations.ai/)' `HorizontalFlip`, and shows the fix: after mirroring coordinates, also swap each left/right pair's row so the label matches the mirrored side. [AlbumentationsX](https://github.com/albumentations-team/AlbumentationsX)'s `KeypointParams(label_mapping=...)` automates exactly this swap inside the transform for larger pipelines — doing it explicitly here shows what that option is doing under the hood." + "For a COCO-17 pose, `sv.KeyPoints.xy` has a fixed row order: row 1 is `left_eye`, row 2 is `right_eye`, and so on for every left/right pair in the skeleton. A horizontal flip mirrors *coordinates*, but on its own it does nothing about *row order* - after mirroring, the position that used to hold the left eye is now where the right eye visually is, but row 1 is still labeled `left_eye`.\n", + "\n", + "This notebook builds a small, deterministic COCO-17 pose (no model or dataset needed), flips it with [Albumentations](https://albumentations.ai/)' `HorizontalFlip`, and shows the fix: after mirroring coordinates, also swap each left/right pair's row so the label matches the mirrored side. [AlbumentationsX](https://github.com/albumentations-team/AlbumentationsX)'s `KeypointParams(label_mapping=...)` automates exactly this swap inside the transform for larger pipelines - doing it explicitly here shows what that option is doing under the hood." + ] + }, + { + "cell_type": "markdown", + "id": "69ab1bd7", + "metadata": {}, + "source": [ + "Click the `Open in Colab` button above to run this cookbook on Google Colab." + ] + }, + { + "cell_type": "markdown", + "id": "77efb21e", + "metadata": {}, + "source": [ + "## Install required packages\n", + "\n", + "This cookbook uses `supervision` for keypoint annotation and `albumentations` for the horizontal flip transform." ] }, { "cell_type": "code", "execution_count": 1, - "id": "cadaa940", + "id": "7f1621ad", "metadata": { "execution": { - "iopub.execute_input": "2026-09-04T12:30:59.505110Z", - "iopub.status.busy": "2026-09-04T12:30:59.504112Z", - "iopub.status.idle": "2026-09-04T12:31:15.554442Z", - "shell.execute_reply": "2026-09-04T12:31:15.553424Z" + "iopub.execute_input": "2026-09-09T15:56:31.909773Z", + "iopub.status.busy": "2026-09-09T15:56:31.909773Z", + "iopub.status.idle": "2026-09-09T15:57:44.766704Z", + "shell.execute_reply": "2026-09-09T15:57:44.764575Z" } }, "outputs": [ @@ -29,11 +49,29 @@ "name": "stderr", "output_type": "stream", "text": [ - "C:\\Users\\sktam\\AppData\\Local\\Temp\\ccwork\\svenv\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", - " from .autonotebook import tqdm as notebook_tqdm\n" + "\n", + "[notice] A new release of pip is available: 25.0.1 -> 26.2.1\n", + "[notice] To update, run: python.exe -m pip install --upgrade pip\n" ] } ], + "source": [ + "!pip install -q supervision==0.30.2 albumentations opencv-python" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "9990e783", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T15:57:44.772798Z", + "iopub.status.busy": "2026-09-09T15:57:44.771704Z", + "iopub.status.idle": "2026-09-09T15:57:51.890825Z", + "shell.execute_reply": "2026-09-09T15:57:51.887817Z" + } + }, + "outputs": [], "source": [ "import albumentations as A\n", "import cv2\n", @@ -43,24 +81,24 @@ }, { "cell_type": "markdown", - "id": "8fa643d7", + "id": "6a476b40", "metadata": {}, "source": [ "## A deterministic pose\n", "\n", - "17 points in COCO order — the order `sv.KeyPoints.from_ultralytics`, `from_inference`, and RF-DETR's pose models already return. The pose has one arm raised so a wrong flip is visibly different from a correct one, not just numerically." + "17 points in COCO-17 order, the order `sv.KeyPoints.from_ultralytics`, `sv.KeyPoints.from_inference`, and RF-DETR's pose models return when the underlying model itself uses COCO-17. These converters preserve whatever skeleton the source model emits, so a different pose model or a custom skeleton may use a different row order than the one hard-coded below. The pose has one arm raised so a wrong flip is visibly different from a correct one, not just numerically." ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "id": "e73b9c50", "metadata": { "execution": { - "iopub.execute_input": "2026-09-04T12:31:15.561491Z", - "iopub.status.busy": "2026-09-04T12:31:15.560603Z", - "iopub.status.idle": "2026-09-04T12:31:15.579433Z", - "shell.execute_reply": "2026-09-04T12:31:15.576902Z" + "iopub.execute_input": "2026-09-09T15:57:51.895170Z", + "iopub.status.busy": "2026-09-09T15:57:51.895170Z", + "iopub.status.idle": "2026-09-09T15:57:51.907513Z", + "shell.execute_reply": "2026-09-09T15:57:51.905508Z" } }, "outputs": [], @@ -102,19 +140,19 @@ "source": [ "## Flip the coordinates\n", "\n", - "`A.HorizontalFlip` mirrors each `(x, y)` pair. It has no idea which row is semantically left or right, so row order is untouched — this is the naive result the issue describes." + "`A.HorizontalFlip` mirrors each `(x, y)` pair. It has no idea which row is semantically left or right, so row order is untouched - this is the naive result the issue describes." ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "id": "69febf39", "metadata": { "execution": { - "iopub.execute_input": "2026-09-04T12:31:15.584447Z", - "iopub.status.busy": "2026-09-04T12:31:15.583450Z", - "iopub.status.idle": "2026-09-04T12:31:15.603397Z", - "shell.execute_reply": "2026-09-04T12:31:15.600818Z" + "iopub.execute_input": "2026-09-09T15:57:51.911569Z", + "iopub.status.busy": "2026-09-09T15:57:51.911569Z", + "iopub.status.idle": "2026-09-09T15:57:51.926077Z", + "shell.execute_reply": "2026-09-09T15:57:51.924047Z" } }, "outputs": [], @@ -146,14 +184,14 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "id": "603b1ee7", "metadata": { "execution": { - "iopub.execute_input": "2026-09-04T12:31:15.610300Z", - "iopub.status.busy": "2026-09-04T12:31:15.610300Z", - "iopub.status.idle": "2026-09-04T12:31:15.622547Z", - "shell.execute_reply": "2026-09-04T12:31:15.620884Z" + "iopub.execute_input": "2026-09-09T15:57:51.931600Z", + "iopub.status.busy": "2026-09-09T15:57:51.931600Z", + "iopub.status.idle": "2026-09-09T15:57:51.942236Z", + "shell.execute_reply": "2026-09-09T15:57:51.940664Z" } }, "outputs": [], @@ -179,14 +217,14 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "id": "0acf5055", "metadata": { "execution": { - "iopub.execute_input": "2026-09-04T12:31:15.627133Z", - "iopub.status.busy": "2026-09-04T12:31:15.627133Z", - "iopub.status.idle": "2026-09-04T12:31:15.640539Z", - "shell.execute_reply": "2026-09-04T12:31:15.638506Z" + "iopub.execute_input": "2026-09-09T15:57:51.948246Z", + "iopub.status.busy": "2026-09-09T15:57:51.947247Z", + "iopub.status.idle": "2026-09-09T15:57:51.960405Z", + "shell.execute_reply": "2026-09-09T15:57:51.959292Z" } }, "outputs": [ @@ -222,19 +260,19 @@ "source": [ "## See the difference\n", "\n", - "Left-side joints in blue, right-side in red. Watch the raised arm: in \"naive flip\" it's still colored red (\"right\") even though mirroring put it on the visual left — exactly the bug. In \"correct flip\" it's blue, matching its true anatomical side after the mirror." + "Left-side joints in blue, right-side in red. Watch the raised arm: in \"naive flip\" it's still colored red (\"right\") even though mirroring put it on the visual left - exactly the bug. In \"correct flip\" it's blue, matching its true anatomical side after the mirror." ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "c676905e", "metadata": { "execution": { - "iopub.execute_input": "2026-09-04T12:31:15.645538Z", - "iopub.status.busy": "2026-09-04T12:31:15.644545Z", - "iopub.status.idle": "2026-09-04T12:31:23.179852Z", - "shell.execute_reply": "2026-09-04T12:31:23.178831Z" + "iopub.execute_input": "2026-09-09T15:57:51.966093Z", + "iopub.status.busy": "2026-09-09T15:57:51.965094Z", + "iopub.status.idle": "2026-09-09T15:57:53.322703Z", + "shell.execute_reply": "2026-09-09T15:57:53.321040Z" } }, "outputs": [ @@ -255,6 +293,7 @@ "\n", "\n", "def render(key_points: sv.KeyPoints, canvas: np.ndarray) -> np.ndarray:\n", + " \"\"\"Draw the skeleton, coloring left-side joints blue and right-side joints red.\"\"\"\n", " scene = sv.EdgeAnnotator(color=sv.Color(200, 200, 200), thickness=2).annotate(\n", " canvas.copy(), key_points\n", " )\n", diff --git a/docs/theme/cookbooks.html b/docs/theme/cookbooks.html index 9f3fbc132f..c92c2de0f0 100644 --- a/docs/theme/cookbooks.html +++ b/docs/theme/cookbooks.html @@ -70,6 +70,10 @@

Supervision Cookbooks

+ +

+
From 2c401c23b0013dec051f4312770535f62880ca84 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 15:59:56 +0000 Subject: [PATCH 3/3] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto=20?= =?UTF-8?q?format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...se-augmentation-left-right-remapping.ipynb | 698 +++++++++--------- 1 file changed, 349 insertions(+), 349 deletions(-) diff --git a/docs/notebooks/pose-augmentation-left-right-remapping.ipynb b/docs/notebooks/pose-augmentation-left-right-remapping.ipynb index 6fb18833c3..a0a82aabc2 100644 --- a/docs/notebooks/pose-augmentation-left-right-remapping.ipynb +++ b/docs/notebooks/pose-augmentation-left-right-remapping.ipynb @@ -1,351 +1,351 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "021ab594", - "metadata": {}, - "source": [ - "# Pose augmentation with left/right keypoint remapping\n", - "\n", - "[![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/roboflow/supervision/blob/develop/docs/notebooks/pose-augmentation-left-right-remapping.ipynb)\n", - "\n", - "For a COCO-17 pose, `sv.KeyPoints.xy` has a fixed row order: row 1 is `left_eye`, row 2 is `right_eye`, and so on for every left/right pair in the skeleton. A horizontal flip mirrors *coordinates*, but on its own it does nothing about *row order* - after mirroring, the position that used to hold the left eye is now where the right eye visually is, but row 1 is still labeled `left_eye`.\n", - "\n", - "This notebook builds a small, deterministic COCO-17 pose (no model or dataset needed), flips it with [Albumentations](https://albumentations.ai/)' `HorizontalFlip`, and shows the fix: after mirroring coordinates, also swap each left/right pair's row so the label matches the mirrored side. [AlbumentationsX](https://github.com/albumentations-team/AlbumentationsX)'s `KeypointParams(label_mapping=...)` automates exactly this swap inside the transform for larger pipelines - doing it explicitly here shows what that option is doing under the hood." - ] - }, - { - "cell_type": "markdown", - "id": "69ab1bd7", - "metadata": {}, - "source": [ - "Click the `Open in Colab` button above to run this cookbook on Google Colab." - ] - }, - { - "cell_type": "markdown", - "id": "77efb21e", - "metadata": {}, - "source": [ - "## Install required packages\n", - "\n", - "This cookbook uses `supervision` for keypoint annotation and `albumentations` for the horizontal flip transform." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "7f1621ad", - "metadata": { - "execution": { - "iopub.execute_input": "2026-09-09T15:56:31.909773Z", - "iopub.status.busy": "2026-09-09T15:56:31.909773Z", - "iopub.status.idle": "2026-09-09T15:57:44.766704Z", - "shell.execute_reply": "2026-09-09T15:57:44.764575Z" - } - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\n", - "[notice] A new release of pip is available: 25.0.1 -> 26.2.1\n", - "[notice] To update, run: python.exe -m pip install --upgrade pip\n" - ] - } - ], - "source": [ - "!pip install -q supervision==0.30.2 albumentations opencv-python" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "9990e783", - "metadata": { - "execution": { - "iopub.execute_input": "2026-09-09T15:57:44.772798Z", - "iopub.status.busy": "2026-09-09T15:57:44.771704Z", - "iopub.status.idle": "2026-09-09T15:57:51.890825Z", - "shell.execute_reply": "2026-09-09T15:57:51.887817Z" - } - }, - "outputs": [], - "source": [ - "import albumentations as A\n", - "import cv2\n", - "import numpy as np\n", - "import supervision as sv" - ] - }, - { - "cell_type": "markdown", - "id": "6a476b40", - "metadata": {}, - "source": [ - "## A deterministic pose\n", - "\n", - "17 points in COCO-17 order, the order `sv.KeyPoints.from_ultralytics`, `sv.KeyPoints.from_inference`, and RF-DETR's pose models return when the underlying model itself uses COCO-17. These converters preserve whatever skeleton the source model emits, so a different pose model or a custom skeleton may use a different row order than the one hard-coded below. The pose has one arm raised so a wrong flip is visibly different from a correct one, not just numerically." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "e73b9c50", - "metadata": { - "execution": { - "iopub.execute_input": "2026-09-09T15:57:51.895170Z", - "iopub.status.busy": "2026-09-09T15:57:51.895170Z", - "iopub.status.idle": "2026-09-09T15:57:51.907513Z", - "shell.execute_reply": "2026-09-09T15:57:51.905508Z" - } - }, - "outputs": [], - "source": [ - "COCO_KEYPOINT_NAMES = [\n", - " \"nose\", \"left_eye\", \"right_eye\", \"left_ear\", \"right_ear\",\n", - " \"left_shoulder\", \"right_shoulder\", \"left_elbow\", \"right_elbow\",\n", - " \"left_wrist\", \"right_wrist\", \"left_hip\", \"right_hip\",\n", - " \"left_knee\", \"right_knee\", \"left_ankle\", \"right_ankle\",\n", - "]\n", - "\n", - "# (left_index, right_index) for every mirrored pair.\n", - "LEFT_RIGHT_PAIRS = [\n", - " (1, 2), (3, 4), (5, 6), (7, 8), (9, 10), (11, 12), (13, 14), (15, 16),\n", - "]\n", - "\n", - "IMAGE_WIDTH, IMAGE_HEIGHT = 400, 400\n", - "\n", - "# right arm raised: an asymmetric pose so a naive flip is visibly wrong.\n", - "POSE = np.array([\n", - " [200, 60], # nose\n", - " [190, 50], [210, 50], # left_eye, right_eye\n", - " [180, 55], [220, 55], # left_ear, right_ear\n", - " [170, 120], [230, 120], # left_shoulder, right_shoulder\n", - " [160, 180], [260, 90], # left_elbow, right_elbow (raised)\n", - " [150, 230], [280, 50], # left_wrist, right_wrist (raised)\n", - " [180, 220], [220, 220], # left_hip, right_hip\n", - " [175, 300], [225, 300], # left_knee, right_knee\n", - " [170, 370], [230, 370], # left_ankle, right_ankle\n", - "], dtype=np.float32)\n", - "\n", - "key_points = sv.KeyPoints(xy=POSE[np.newaxis, :, :], class_id=np.array([0]))" - ] - }, - { - "cell_type": "markdown", - "id": "baa69f4f", - "metadata": {}, - "source": [ - "## Flip the coordinates\n", - "\n", - "`A.HorizontalFlip` mirrors each `(x, y)` pair. It has no idea which row is semantically left or right, so row order is untouched - this is the naive result the issue describes." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "69febf39", - "metadata": { - "execution": { - "iopub.execute_input": "2026-09-09T15:57:51.911569Z", - "iopub.status.busy": "2026-09-09T15:57:51.911569Z", - "iopub.status.idle": "2026-09-09T15:57:51.926077Z", - "shell.execute_reply": "2026-09-09T15:57:51.924047Z" - } - }, - "outputs": [], - "source": [ - "transform = A.Compose(\n", - " [A.HorizontalFlip(p=1.0)],\n", - " keypoint_params=A.KeypointParams(format=\"xy\", remove_invisible=False),\n", - ")\n", - "\n", - "blank_image = np.zeros((IMAGE_HEIGHT, IMAGE_WIDTH, 3), dtype=np.uint8)\n", - "mirrored = transform(image=blank_image, keypoints=key_points.xy[0].tolist())\n", - "mirrored_xy = np.array(mirrored[\"keypoints\"], dtype=np.float32)\n", - "\n", - "# Rebuilt straight from the mirrored coordinates: coordinates are correct,\n", - "# but row order still says row 1 is \"left_eye\" even though its mirrored\n", - "# position is now on the visual right of the frame.\n", - "naive_flip = sv.KeyPoints(xy=mirrored_xy[np.newaxis, :, :], class_id=np.array([0]))" - ] - }, - { - "cell_type": "markdown", - "id": "8f76c009", - "metadata": {}, - "source": [ - "## Fix: swap each pair's row after mirroring\n", - "\n", - "This is the step AlbumentationsX's `KeypointParams(label_mapping=...)` automates. Doing it by hand here: build the permutation once from `LEFT_RIGHT_PAIRS`, then rebuild `sv.KeyPoints` with that row order applied to the mirrored coordinates." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "603b1ee7", - "metadata": { - "execution": { - "iopub.execute_input": "2026-09-09T15:57:51.931600Z", - "iopub.status.busy": "2026-09-09T15:57:51.931600Z", - "iopub.status.idle": "2026-09-09T15:57:51.942236Z", - "shell.execute_reply": "2026-09-09T15:57:51.940664Z" - } - }, - "outputs": [], - "source": [ - "remap = list(range(len(COCO_KEYPOINT_NAMES)))\n", - "for left, right in LEFT_RIGHT_PAIRS:\n", - " remap[left], remap[right] = remap[right], remap[left]\n", - "\n", - "correct_flip = sv.KeyPoints(\n", - " xy=mirrored_xy[remap][np.newaxis, :, :], class_id=np.array([0])\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "90757710", - "metadata": {}, - "source": [ - "## Verify\n", - "\n", - "For every pair, the remapped left row should sit exactly where the *original* right row mirrors to, and vice versa. Albumentations mirrors pixel index `i` to `(width - 1 - i)`, so that's the formula to check against rather than a bare `width - x`." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "0acf5055", - "metadata": { - "execution": { - "iopub.execute_input": "2026-09-09T15:57:51.948246Z", - "iopub.status.busy": "2026-09-09T15:57:51.947247Z", - "iopub.status.idle": "2026-09-09T15:57:51.960405Z", - "shell.execute_reply": "2026-09-09T15:57:51.959292Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "All left/right pairs land where they should.\n" - ] - } - ], - "source": [ - "MIRROR_X = IMAGE_WIDTH - 1\n", - "\n", - "for left, right in LEFT_RIGHT_PAIRS:\n", - " expected_left = [MIRROR_X - POSE[right][0], POSE[right][1]]\n", - " assert np.allclose(correct_flip.xy[0][left], expected_left), COCO_KEYPOINT_NAMES[left]\n", - "\n", - " expected_right = [MIRROR_X - POSE[left][0], POSE[left][1]]\n", - " assert np.allclose(correct_flip.xy[0][right], expected_right), COCO_KEYPOINT_NAMES[right]\n", - "\n", - "# And confirm the naive version actually is wrong for this asymmetric pose,\n", - "# not just differently-labeled-but-coincidentally-equal:\n", - "assert not np.allclose(naive_flip.xy[0][9], correct_flip.xy[0][9])\n", - "\n", - "print(\"All left/right pairs land where they should.\")" - ] - }, - { - "cell_type": "markdown", - "id": "93e3c80d", - "metadata": {}, - "source": [ - "## See the difference\n", - "\n", - "Left-side joints in blue, right-side in red. Watch the raised arm: in \"naive flip\" it's still colored red (\"right\") even though mirroring put it on the visual left - exactly the bug. In \"correct flip\" it's blue, matching its true anatomical side after the mirror." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "c676905e", - "metadata": { - "execution": { - "iopub.execute_input": "2026-09-09T15:57:51.966093Z", - "iopub.status.busy": "2026-09-09T15:57:51.965094Z", - "iopub.status.idle": "2026-09-09T15:57:53.322703Z", - "shell.execute_reply": "2026-09-09T15:57:53.321040Z" - } - }, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA7AAAAFICAYAAABp4xKfAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvctoD+AAAAAlwSFlzAAAPYQAAD2EBqD+naQAAUmZJREFUeJzt3Qd0HNXZ//FntqvLKpYbCPeKwTYlmJgWWugJ9U1CKHkJ2IHQQgk1EEgjocdAAFMTXtofQguQEFOMTQm26QZckLskq5fd1Wp3/ue5thxjS6uunZG+n3N0vNauVtdjzU/3mVvGsm3bFgAAAAAAHM6T6gYAAAAAANARFLAAAAAAAFeggAUAAAAAuAIFLAAAAADAFShgAQAAAACuQAELAAAAAHAFClgAAAAAgCtQwAIAAAAAXIECFgAAAADgCr6OvrC4uLh3WwIAAAAAGJBKSko69DpGYAEAAAAArkABCwAAAABwBQpYAAAAAIArUMACAAAAAFyBAhYAAAAA0L92Ie4pfr9fPB6PRKPRNl+TlpYmTU1NEo/Hu/T+tm1Lc3NzN1sKAB3n8/nE6/UmzbaO0PfIzMyUQCAgDQ0NJgs10zTb9L31MQC4LR81u7bv11mWJaFQSLKysky+hcNhicVi5nP6J305AI4oYI899lgZMmSI3HXXXa0WqFq83nDDDfLQQw/JRx991On3P+WUU6Smpkaee+65HmoxALTvoIMOkl133VX+/Oc/SyQS6fIhO+644+T000+XFStWmCz74IMPZMOGDXLIIYfI3Llzpbq6mv8OAK6hF9++973vSVVVlbz22muSSCS2Pjds2DC5/PLLzZ/vvPOOee7xxx+Xc889V15++WV5/fXXU9p2AM7U51OIP/nkk60h1Rq94vbSSy/Jxo0bu/T+48aNk9GjR3ezlQDQOV999ZUsWLDAZFh3O3pPP/20XHXVVWbUQvNs7dq1piPXncIYAFJBZ5WMGTNGdt55ZzPiui3Nt9zcXLnyyivl73//u0ybNs3MQHnllVdk1apV/IcB6N0RWA2l/fff34wSKA2ft99+W8aPHy9Tp041HbH8/HzZtGmTuQqn04inTJkixx9/vHlORxkyMjLMFTcNNO0M/s///I/U1tbKhAkTzNW5hQsXyjPPPGOKXw28M844Q0aNGmWK3QceeEDWr1/fU/8cAAOYZplmzNChQ2Xs2LHy5ZdfysMPP2ymt+kskRNOOEH23HNPk09PPPGEmS2Sl5dnXv+d73xH0tPT5dlnnzXvVVRUJIceeqi8+uqrZorcSSedZHLx888/l6eeemrriKq+TkdeNe+0CNb3KCwsNJmp0+tGjBghn332mfz4xz+WiooK09HTHNXZJu+++y5TiwG0SnNCc0lniehFspaRTZ0FpzlyxBFHSE5OjsyfP998aN7o5xobG2XkyJGmP6ZLGrSvVlBQYPphxcXFpr+n7/H888/LkiVLTAbNmDHDfK3299566y3zvXRm3H777Wfeb6eddpL7779f1qxZY2bknXjiieZzZ511lixevHhrm7VvV1paKgceeKBpjxa/2jfULH7wwQdNFgMYuHpsBFYD6+KLL5YvvvjCBMwvf/lL2WeffUzo6Oe/9a1vyerVq00Y7r333ib8rrjiCtOhW7RokRxzzDGmINVO42GHHWamGWvgXXbZZVJfXy/Lli2T2bNny9FHH22+35lnnimDBg2Sxx57zATrb37zG/MnAHTX5MmT5ZprrpHs7GzTqTryyCPl7LPP3pp1WrzqNDe9eKbZo6/TEYZ9993XjJJqgaudMr2wpx1Efb3SqXKafU8++aS5KHfppZea0Qal612XLl1qimItiPWxfk4NHz7c5KG+VgtYvfCn7SovLzcZqRcJAaA1Rx11lJx//vmmH/Xxxx+bPpn2w6ZPn276YZo5ehFM+2C6hEEvxs2ZM8fkng4MaHGrX6N9Ou3HzZw50zz/5ptvmvfTHNOLcpp12verq6szz2lxqkWqft9169aZEVX9Pro0Qi1fvtz0GcvKyszndaZJCy2OtYjdbbfd5OqrrzbFs76nFuI///nP+Y8GBrgeGYHVwlE7aDri+uijj5qrcHqlTAtR/VxJSYn8+te/NlfTdJRBO2NayGpHr2XkVIvUCy+80HT49GphC51ufM8995jH+nUaujrNRDuAeiVRRzx0NEI7jhq6ANBdmkMrV66UW265xYyGaiZpx+62224znaj333/fZI8WmJo7Woy2fJ2OVmgHTtfDasdMRxBaRhb22msvuf32283X6ejEz372M9lll13M0grNQJ1lojNU9KKeZt93v/vdre/bMvVOv1bzT0dB9CKeLpvQUWItetngCcC2gsGgGRXVbHnkkUfM53RNvW6OpBfqdCRU+2GaPzqAcMABB5gs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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "LEFT_INDICES = {left for left, _ in LEFT_RIGHT_PAIRS}\n", - "RIGHT_INDICES = {right for _, right in LEFT_RIGHT_PAIRS}\n", - "\n", - "\n", - "def render(key_points: sv.KeyPoints, canvas: np.ndarray) -> np.ndarray:\n", - " \"\"\"Draw the skeleton, coloring left-side joints blue and right-side joints red.\"\"\"\n", - " scene = sv.EdgeAnnotator(color=sv.Color(200, 200, 200), thickness=2).annotate(\n", - " canvas.copy(), key_points\n", - " )\n", - "\n", - " num_points = key_points.xy.shape[1]\n", - " left_mask = np.array([[i in LEFT_INDICES for i in range(num_points)]])\n", - " right_mask = np.array([[i in RIGHT_INDICES for i in range(num_points)]])\n", - "\n", - " left_kp = sv.KeyPoints(xy=key_points.xy, class_id=key_points.class_id, visible=left_mask)\n", - " right_kp = sv.KeyPoints(xy=key_points.xy, class_id=key_points.class_id, visible=right_mask)\n", - "\n", - " scene = sv.VertexAnnotator(color=sv.Color(0, 100, 255), radius=6).annotate(scene, left_kp)\n", - " scene = sv.VertexAnnotator(color=sv.Color(255, 0, 0), radius=6).annotate(scene, right_kp)\n", - " return scene\n", - "\n", - "\n", - "panels = [\n", - " (\"original\", key_points),\n", - " (\"naive flip\", naive_flip),\n", - " (\"correct flip\", correct_flip),\n", - "]\n", - "canvas = np.full((IMAGE_HEIGHT, IMAGE_WIDTH, 3), 30, dtype=np.uint8)\n", - "images = [render(kp, canvas) for _, kp in panels]\n", - "combined = np.concatenate(images, axis=1)\n", - "for i, (title, _) in enumerate(panels):\n", - " cv2.putText(\n", - " combined, title, (i * IMAGE_WIDTH + 10, 20),\n", - " cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1, cv2.LINE_AA,\n", - " )\n", - "\n", - "sv.plot_image(combined, size=(12, 4))" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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.10" - } - }, - "nbformat": 4, - "nbformat_minor": 5 + "cells": [ + { + "cell_type": "markdown", + "id": "021ab594", + "metadata": {}, + "source": [ + "# Pose augmentation with left/right keypoint remapping\n", + "\n", + "[![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/roboflow/supervision/blob/develop/docs/notebooks/pose-augmentation-left-right-remapping.ipynb)\n", + "\n", + "For a COCO-17 pose, `sv.KeyPoints.xy` has a fixed row order: row 1 is `left_eye`, row 2 is `right_eye`, and so on for every left/right pair in the skeleton. A horizontal flip mirrors *coordinates*, but on its own it does nothing about *row order* - after mirroring, the position that used to hold the left eye is now where the right eye visually is, but row 1 is still labeled `left_eye`.\n", + "\n", + "This notebook builds a small, deterministic COCO-17 pose (no model or dataset needed), flips it with [Albumentations](https://albumentations.ai/)' `HorizontalFlip`, and shows the fix: after mirroring coordinates, also swap each left/right pair's row so the label matches the mirrored side. [AlbumentationsX](https://github.com/albumentations-team/AlbumentationsX)'s `KeypointParams(label_mapping=...)` automates exactly this swap inside the transform for larger pipelines - doing it explicitly here shows what that option is doing under the hood." + ] + }, + { + "cell_type": "markdown", + "id": "69ab1bd7", + "metadata": {}, + "source": [ + "Click the `Open in Colab` button above to run this cookbook on Google Colab." + ] + }, + { + "cell_type": "markdown", + "id": "77efb21e", + "metadata": {}, + "source": [ + "## Install required packages\n", + "\n", + "This cookbook uses `supervision` for keypoint annotation and `albumentations` for the horizontal flip transform." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "7f1621ad", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T15:56:31.909773Z", + "iopub.status.busy": "2026-09-09T15:56:31.909773Z", + "iopub.status.idle": "2026-09-09T15:57:44.766704Z", + "shell.execute_reply": "2026-09-09T15:57:44.764575Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + "[notice] A new release of pip is available: 25.0.1 -> 26.2.1\n", + "[notice] To update, run: python.exe -m pip install --upgrade pip\n" + ] + } + ], + "source": [ + "!pip install -q supervision==0.30.2 albumentations opencv-python" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "9990e783", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T15:57:44.772798Z", + "iopub.status.busy": "2026-09-09T15:57:44.771704Z", + "iopub.status.idle": "2026-09-09T15:57:51.890825Z", + "shell.execute_reply": "2026-09-09T15:57:51.887817Z" + } + }, + "outputs": [], + "source": [ + "import albumentations as A\n", + "import cv2\n", + "import numpy as np\n", + "import supervision as sv" + ] + }, + { + "cell_type": "markdown", + "id": "6a476b40", + "metadata": {}, + "source": [ + "## A deterministic pose\n", + "\n", + "17 points in COCO-17 order, the order `sv.KeyPoints.from_ultralytics`, `sv.KeyPoints.from_inference`, and RF-DETR's pose models return when the underlying model itself uses COCO-17. These converters preserve whatever skeleton the source model emits, so a different pose model or a custom skeleton may use a different row order than the one hard-coded below. The pose has one arm raised so a wrong flip is visibly different from a correct one, not just numerically." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "e73b9c50", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T15:57:51.895170Z", + "iopub.status.busy": "2026-09-09T15:57:51.895170Z", + "iopub.status.idle": "2026-09-09T15:57:51.907513Z", + "shell.execute_reply": "2026-09-09T15:57:51.905508Z" + } + }, + "outputs": [], + "source": [ + "COCO_KEYPOINT_NAMES = [\n", + " \"nose\", \"left_eye\", \"right_eye\", \"left_ear\", \"right_ear\",\n", + " \"left_shoulder\", \"right_shoulder\", \"left_elbow\", \"right_elbow\",\n", + " \"left_wrist\", \"right_wrist\", \"left_hip\", \"right_hip\",\n", + " \"left_knee\", \"right_knee\", \"left_ankle\", \"right_ankle\",\n", + "]\n", + "\n", + "# (left_index, right_index) for every mirrored pair.\n", + "LEFT_RIGHT_PAIRS = [\n", + " (1, 2), (3, 4), (5, 6), (7, 8), (9, 10), (11, 12), (13, 14), (15, 16),\n", + "]\n", + "\n", + "IMAGE_WIDTH, IMAGE_HEIGHT = 400, 400\n", + "\n", + "# right arm raised: an asymmetric pose so a naive flip is visibly wrong.\n", + "POSE = np.array([\n", + " [200, 60], # nose\n", + " [190, 50], [210, 50], # left_eye, right_eye\n", + " [180, 55], [220, 55], # left_ear, right_ear\n", + " [170, 120], [230, 120], # left_shoulder, right_shoulder\n", + " [160, 180], [260, 90], # left_elbow, right_elbow (raised)\n", + " [150, 230], [280, 50], # left_wrist, right_wrist (raised)\n", + " [180, 220], [220, 220], # left_hip, right_hip\n", + " [175, 300], [225, 300], # left_knee, right_knee\n", + " [170, 370], [230, 370], # left_ankle, right_ankle\n", + "], dtype=np.float32)\n", + "\n", + "key_points = sv.KeyPoints(xy=POSE[np.newaxis, :, :], class_id=np.array([0]))" + ] + }, + { + "cell_type": "markdown", + "id": "baa69f4f", + "metadata": {}, + "source": [ + "## Flip the coordinates\n", + "\n", + "`A.HorizontalFlip` mirrors each `(x, y)` pair. It has no idea which row is semantically left or right, so row order is untouched - this is the naive result the issue describes." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "69febf39", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T15:57:51.911569Z", + "iopub.status.busy": "2026-09-09T15:57:51.911569Z", + "iopub.status.idle": "2026-09-09T15:57:51.926077Z", + "shell.execute_reply": "2026-09-09T15:57:51.924047Z" + } + }, + "outputs": [], + "source": [ + "transform = A.Compose(\n", + " [A.HorizontalFlip(p=1.0)],\n", + " keypoint_params=A.KeypointParams(format=\"xy\", remove_invisible=False),\n", + ")\n", + "\n", + "blank_image = np.zeros((IMAGE_HEIGHT, IMAGE_WIDTH, 3), dtype=np.uint8)\n", + "mirrored = transform(image=blank_image, keypoints=key_points.xy[0].tolist())\n", + "mirrored_xy = np.array(mirrored[\"keypoints\"], dtype=np.float32)\n", + "\n", + "# Rebuilt straight from the mirrored coordinates: coordinates are correct,\n", + "# but row order still says row 1 is \"left_eye\" even though its mirrored\n", + "# position is now on the visual right of the frame.\n", + "naive_flip = sv.KeyPoints(xy=mirrored_xy[np.newaxis, :, :], class_id=np.array([0]))" + ] + }, + { + "cell_type": "markdown", + "id": "8f76c009", + "metadata": {}, + "source": [ + "## Fix: swap each pair's row after mirroring\n", + "\n", + "This is the step AlbumentationsX's `KeypointParams(label_mapping=...)` automates. Doing it by hand here: build the permutation once from `LEFT_RIGHT_PAIRS`, then rebuild `sv.KeyPoints` with that row order applied to the mirrored coordinates." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "603b1ee7", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T15:57:51.931600Z", + "iopub.status.busy": "2026-09-09T15:57:51.931600Z", + "iopub.status.idle": "2026-09-09T15:57:51.942236Z", + "shell.execute_reply": "2026-09-09T15:57:51.940664Z" + } + }, + "outputs": [], + "source": [ + "remap = list(range(len(COCO_KEYPOINT_NAMES)))\n", + "for left, right in LEFT_RIGHT_PAIRS:\n", + " remap[left], remap[right] = remap[right], remap[left]\n", + "\n", + "correct_flip = sv.KeyPoints(\n", + " xy=mirrored_xy[remap][np.newaxis, :, :], class_id=np.array([0])\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "90757710", + "metadata": {}, + "source": [ + "## Verify\n", + "\n", + "For every pair, the remapped left row should sit exactly where the *original* right row mirrors to, and vice versa. Albumentations mirrors pixel index `i` to `(width - 1 - i)`, so that's the formula to check against rather than a bare `width - x`." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0acf5055", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T15:57:51.948246Z", + "iopub.status.busy": "2026-09-09T15:57:51.947247Z", + "iopub.status.idle": "2026-09-09T15:57:51.960405Z", + "shell.execute_reply": "2026-09-09T15:57:51.959292Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "All left/right pairs land where they should.\n" + ] + } + ], + "source": [ + "MIRROR_X = IMAGE_WIDTH - 1\n", + "\n", + "for left, right in LEFT_RIGHT_PAIRS:\n", + " expected_left = [MIRROR_X - POSE[right][0], POSE[right][1]]\n", + " assert np.allclose(correct_flip.xy[0][left], expected_left), COCO_KEYPOINT_NAMES[left]\n", + "\n", + " expected_right = [MIRROR_X - POSE[left][0], POSE[left][1]]\n", + " assert np.allclose(correct_flip.xy[0][right], expected_right), COCO_KEYPOINT_NAMES[right]\n", + "\n", + "# And confirm the naive version actually is wrong for this asymmetric pose,\n", + "# not just differently-labeled-but-coincidentally-equal:\n", + "assert not np.allclose(naive_flip.xy[0][9], correct_flip.xy[0][9])\n", + "\n", + "print(\"All left/right pairs land where they should.\")" + ] + }, + { + "cell_type": "markdown", + "id": "93e3c80d", + "metadata": {}, + "source": [ + "## See the difference\n", + "\n", + "Left-side joints in blue, right-side in red. Watch the raised arm: in \"naive flip\" it's still colored red (\"right\") even though mirroring put it on the visual left - exactly the bug. In \"correct flip\" it's blue, matching its true anatomical side after the mirror." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "c676905e", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T15:57:51.966093Z", + "iopub.status.busy": "2026-09-09T15:57:51.965094Z", + "iopub.status.idle": "2026-09-09T15:57:53.322703Z", + "shell.execute_reply": "2026-09-09T15:57:53.321040Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA7AAAAFICAYAAABp4xKfAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvctoD+AAAAAlwSFlzAAAPYQAAD2EBqD+naQAAUmZJREFUeJzt3Qd0HNXZ//FntqvLKpYbCPeKwTYlmJgWWugJ9U1CKHkJ2IHQQgk1EEgjocdAAFMTXtofQguQEFOMTQm26QZckLskq5fd1Wp3/ue5thxjS6uunZG+n3N0vNauVtdjzU/3mVvGsm3bFgAAAAAAHM6T6gYAAAAAANARFLAAAAAAAFeggAUAAAAAuAIFLAAAAADAFShgAQAAAACuQAELAAAAAHAFClgAAAAAgCtQwAIAAAAAXIECFgAAAADgCr6OvrC4uLh3WwIAAAAAGJBKSko69DpGYAEAAAAArkABCwAAAABwBQpYAAAAAIArUMACAAAAAFyBAhYAAAAA0L92Ie4pfr9fPB6PRKPRNl+TlpYmTU1NEo/Hu/T+tm1Lc3NzN1sKAB3n8/nE6/UmzbaO0PfIzMyUQCAgDQ0NJgs10zTb9L31MQC4LR81u7bv11mWJaFQSLKysky+hcNhicVi5nP6J305AI4oYI899lgZMmSI3HXXXa0WqFq83nDDDfLQQw/JRx991On3P+WUU6Smpkaee+65HmoxALTvoIMOkl133VX+/Oc/SyQS6fIhO+644+T000+XFStWmCz74IMPZMOGDXLIIYfI3Llzpbq6mv8OAK6hF9++973vSVVVlbz22muSSCS2Pjds2DC5/PLLzZ/vvPOOee7xxx+Xc889V15++WV5/fXXU9p2AM7U51OIP/nkk60h1Rq94vbSSy/Jxo0bu/T+48aNk9GjR3ezlQDQOV999ZUsWLDAZFh3O3pPP/20XHXVVWbUQvNs7dq1piPXncIYAFJBZ5WMGTNGdt55ZzPiui3Nt9zcXLnyyivl73//u0ybNs3MQHnllVdk1apV/IcB6N0RWA2l/fff34wSKA2ft99+W8aPHy9Tp041HbH8/HzZtGmTuQqn04inTJkixx9/vHlORxkyMjLMFTcNNO0M/s///I/U1tbKhAkTzNW5hQsXyjPPPGOKXw28M844Q0aNGmWK3QceeEDWr1/fU/8cAAOYZplmzNChQ2Xs2LHy5ZdfysMPP2ymt+kskRNOOEH23HNPk09PPPGEmS2Sl5dnXv+d73xH0tPT5dlnnzXvVVRUJIceeqi8+uqrZorcSSedZHLx888/l6eeemrriKq+TkdeNe+0CNb3KCwsNJmp0+tGjBghn332mfz4xz+WiooK09HTHNXZJu+++y5TiwG0SnNCc0lniehFspaRTZ0FpzlyxBFHSE5OjsyfP998aN7o5xobG2XkyJGmP6ZLGrSvVlBQYPphxcXFpr+n7/H888/LkiVLTAbNmDHDfK3299566y3zvXRm3H777Wfeb6eddpL7779f1qxZY2bknXjiieZzZ511lixevHhrm7VvV1paKgceeKBpjxa/2jfULH7wwQdNFgMYuHpsBFYD6+KLL5YvvvjCBMwvf/lL2WeffUzo6Oe/9a1vyerVq00Y7r333ib8rrjiCtOhW7RokRxzzDGmINVO42GHHWamGWvgXXbZZVJfXy/Lli2T2bNny9FHH22+35lnnimDBg2Sxx57zATrb37zG/MnAHTX5MmT5ZprrpHs7GzTqTryyCPl7LPP3pp1WrzqNDe9eKbZo6/TEYZ9993XjJJqgaudMr2wpx1Efb3SqXKafU8++aS5KHfppZea0Qal612XLl1qimItiPWxfk4NHz7c5KG+VgtYvfCn7SovLzcZqRcJAaA1Rx11lJx//vmmH/Xxxx+bPpn2w6ZPn276YZo5ehFM+2C6hEEvxs2ZM8fkng4MaHGrX6N9Ou3HzZw50zz/5ptvmvfTHNOLcpp12verq6szz2lxqkWqft9169aZEVX9Pro0Qi1fvtz0GcvKyszndaZJCy2OtYjdbbfd5OqrrzbFs76nFuI///nP+Y8GBrgeGYHVwlE7aDri+uijj5qrcHqlTAtR/VxJSYn8+te/NlfTdJRBO2NayGpHr2XkVIvUCy+80HT49GphC51ufM8995jH+nUaujrNRDuAeiVRRzx0NEI7jhq6ANBdmkMrV66UW265xYyGaiZpx+62224znaj333/fZI8WmJo7Woy2fJ2OVmgHTtfDasdMRxBaRhb22msvuf32283X6ejEz372M9lll13M0grNQJ1lojNU9KKeZt93v/vdre/bMvVOv1bzT0dB9CKeLpvQUWItetngCcC2gsGgGRXVbHnkkUfM53RNvW6OpBfqdCRU+2GaPzqAcMABB5gs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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "LEFT_INDICES = {left for left, _ in LEFT_RIGHT_PAIRS}\n", + "RIGHT_INDICES = {right for _, right in LEFT_RIGHT_PAIRS}\n", + "\n", + "\n", + "def render(key_points: sv.KeyPoints, canvas: np.ndarray) -> np.ndarray:\n", + " \"\"\"Draw the skeleton, coloring left-side joints blue and right-side joints red.\"\"\"\n", + " scene = sv.EdgeAnnotator(color=sv.Color(200, 200, 200), thickness=2).annotate(\n", + " canvas.copy(), key_points\n", + " )\n", + "\n", + " num_points = key_points.xy.shape[1]\n", + " left_mask = np.array([[i in LEFT_INDICES for i in range(num_points)]])\n", + " right_mask = np.array([[i in RIGHT_INDICES for i in range(num_points)]])\n", + "\n", + " left_kp = sv.KeyPoints(xy=key_points.xy, class_id=key_points.class_id, visible=left_mask)\n", + " right_kp = sv.KeyPoints(xy=key_points.xy, class_id=key_points.class_id, visible=right_mask)\n", + "\n", + " scene = sv.VertexAnnotator(color=sv.Color(0, 100, 255), radius=6).annotate(scene, left_kp)\n", + " scene = sv.VertexAnnotator(color=sv.Color(255, 0, 0), radius=6).annotate(scene, right_kp)\n", + " return scene\n", + "\n", + "\n", + "panels = [\n", + " (\"original\", key_points),\n", + " (\"naive flip\", naive_flip),\n", + " (\"correct flip\", correct_flip),\n", + "]\n", + "canvas = np.full((IMAGE_HEIGHT, IMAGE_WIDTH, 3), 30, dtype=np.uint8)\n", + "images = [render(kp, canvas) for _, kp in panels]\n", + "combined = np.concatenate(images, axis=1)\n", + "for i, (title, _) in enumerate(panels):\n", + " cv2.putText(\n", + " combined, title, (i * IMAGE_WIDTH + 10, 20),\n", + " cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1, cv2.LINE_AA,\n", + " )\n", + "\n", + "sv.plot_image(combined, size=(12, 4))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 }