kodokan
kodokan — study Kodokan Judo throws from video via body-pose analysis.
A pipeline over the official Kodokan 100 Techniques YouTube playlist: acquire clips + metadata, estimate per-frame two-person skeletons, segment each clip into its repeated demonstrations, visualize skeletons (overlay-on-video and on a blank canvas), and — later — recognize, compare, and score throws.
Quick start:
from kodokan.acquire import download_techniques
from kodokan.pose import estimate_poses
from kodokan.viz import render_skeleton_video
res = download_techniques(playlist_items="2") # Seoi-nage
seq = estimate_poses(res[0].path, source_url=res[0].info["webpage_url"])
render_skeleton_video(seq, out_path="overlay.mp4", source_video=res[0].path)
render_skeleton_video(seq, out_path="skeleton.mp4", blank_canvas=True)
See misc/docs/research-architecture.md for the tool/architecture rationale.
- class kodokan.PoseSequence(keypoints: ndarray, frame_indices: ndarray, fps: float, width: int, height: int, backend: str, video_path: str, source_url: str | None = None, keypoint_names: tuple[str, ...] = ('nose', 'left_eye', 'right_eye', 'left_ear', 'right_ear', 'left_shoulder', 'right_shoulder', 'left_elbow', 'right_elbow', 'left_wrist', 'right_wrist', 'left_hip', 'right_hip', 'left_knee', 'right_knee', 'left_ankle', 'right_ankle'), skeleton: tuple[tuple[int, int], ...] = ((5, 7), (7, 9), (6, 8), (8, 10), (5, 6), (5, 11), (6, 12), (11, 12), (11, 13), (13, 15), (12, 14), (14, 16), (0, 1), (0, 2), (1, 3), (2, 4), (0, 5), (0, 6)))[source]
A per-frame, per-person sequence of COCO-17 keypoints for one clip.
- keypoints
(F, P, 17, 3)float array of (x_px, y_px, confidence);NaNfor empty person slots.- Type:
numpy.ndarray
- frame_indices
(F,)int array of source frame indices analyzed.- Type:
numpy.ndarray
- fps
Source frames-per-second.
- Type:
float
- width, height
Source frame dimensions in pixels.
- backend
The estimator backend used.
- Type:
str
- video_path
Source clip path (provenance).
- Type:
str
- source_url
Canonical YouTube URL, if known (provenance; required by project).
- Type:
str | None
- class kodokan.Segment(index: int, start_s: float, end_s: float, start_frame: int, end_frame: int, peak_activity: float, two_person_frac: float | None = None)[source]
One detected demonstration interval.
- kodokan.angle_features(pose_seq: PoseSequence, *, person: int = 0, frame_range: tuple[int, int] | None = None) ndarray[source]
Per-frame joint-angle features
(F, n_angles)for one person.
- kodokan.build_catalog(*, min_two_person_frac: float | None = None, min_demo_s: float = 1.0)[source]
Build
{technique_key: {"name", "clips":[...]}}from the pose/segments stores.Aggregates all sources: each clip contributes its demo intervals (for looping media), so a technique present in several sources offers varied multiple-choice media.
- kodokan.build_confusability(catalog: dict | None = None, *, feature: str = 'tori_angles_pos') dict[source]
Technique×technique similarity (0..1) from mean pooled pose descriptors.
similarity[a][b]high ⇒ techniques a, b look alike in pose-space (likely confusable for a learner). An honest, recognizer-free proxy; swap for the model’s confusion matrix once cross-source recognition works.
- kodokan.build_reference(feature_list: list[ndarray]) dict[source]
Pick the medoid demo as reference and record the baseline distance spread.
Returns
{medoid, reference, baseline, distance_matrix}wherebaselineis the array of normalized-DTW distances from every other demo to the medoid.
- kodokan.compare(features_a: ndarray, features_b: ndarray) dict[source]
DTW-align two angle-feature sequences.
Returns a dict with
distance(total DTW cost),normalized(cost per aligned step — comparable across different-length demos), the warpingpath, and the cleaned input arraysa/b.
- kodokan.demo_features(pose_seq: PoseSequence, start_s: float, end_s: float, *, person: int | None = None) ndarray[source]
Cleaned joint-angle features for one demo window (primary person by default).
- kodokan.distance_matrix(feature_seqs: list[ndarray]) ndarray[source]
Pairwise normalized-DTW distance matrix over a list of feature sequences.
- kodokan.estimate_poses(video_path: str | Path, *, backend: str = 'rtmlib', n_persons: int = 2, conf_thresh: float = 0.3, frame_step: int = 1, frame_range: tuple[int, int] | None = None, device: str | None = None, source_url: str | None = None, progress: bool = True, **backend_kwargs) PoseSequence[source]
Estimate per-frame, per-person COCO-17 keypoints for a video clip.
Simplest use:
estimate_poses("clip.mp4")→ aPoseSequencewith the two highest-confidence people per frame (left→right ordered) via RTMPose.- Parameters:
video_path – Path to the video clip.
backend –
"rtmlib"(RTMPose top-down, CPU/ONNX; default, and the only one installed bykodokan[pose]) or"ultralytics"(YOLO11-pose, MPS), which needskodokan[track]— an AGPL-3.0-or-later dependency, see the README’s “Licensing of extras”.n_persons – Number of person slots to keep per frame (2 for tori+uke).
conf_thresh – Minimum mean per-person confidence to keep a detection.
frame_step – Analyze every
frame_step-th frame (1 = every frame).frame_range – Optional
(start, stop)frame index window.device – Backend device (
"cpu"/"mps"); backend default ifNone.source_url – Canonical source URL to record as provenance.
progress – Print a progress line periodically.
**backend_kwargs – Forwarded to the backend (e.g.
mode="performance"for rtmlib, ormodel_name="yolo11m-pose.pt"for ultralytics).
- Returns:
A
PoseSequenceof shape(F, n_persons, 17, 3).
- kodokan.estimate_poses_tracked(video_path: str | Path, *, n_persons: int = 2, tracker: str = 'botsort.yaml', conf_thresh: float = 0.3, max_gap_frac: float = 0.15, stale_after: int = 12, frame_step: int = 1, frame_range: tuple[int, int] | None = None, device: str | None = 'mps', model_name: str = 'yolo11n-pose.pt', source_url: str | None = None, progress: bool = True) PoseSequence[source]
Estimate per-frame keypoints with persistent tori/uke identity.
Returns a
PoseSequencewhose person slots are stable across the clip (slot 0 = the track that is, on average, further left).- Parameters:
video_path – Path to the clip.
n_persons – Number of stable identity slots to keep (2 for tori+uke).
tracker – Ultralytics tracker config (
"botsort.yaml"or"bytetrack.yaml").conf_thresh – Minimum mean per-person confidence to count a detection.
frame_step – Analyze every n-th frame.
frame_range – Optional
(start, stop)frame window.device – Torch device (
"mps"/"cpu").model_name – YOLO-pose weights (resolved under the data models dir).
source_url – Provenance URL.
progress – Print progress.
- kodokan.feedback(query_features: ndarray, reference: dict, *, n_phases: int = 3) dict[source]
Interpretable difference: per joint-angle and per phase (degrees).
- kodokan.load_all_tidy(store=None)[source]
Concatenate every clip’s tidy table (with a
video_idcolumn) into one DataFrame.
- kodokan.log_response(problem: Problem, chosen_index: int, *, user: str = 'default', store=None, similarity=None, timestamp: str | None = None) dict[source]
Record a response (datetime + exact problem + choice + correctness + score).
- kodokan.log_to_rerun(pose_seq: PoseSequence, *, source_video: str | Path | None = None, save: str | Path | None = None, spawn: bool = False, blank_canvas: bool = True, frame_scale: float = 0.5, conf_thresh: float = 0.3, entity_prefix: str = '') None[source]
Log frames + 2D skeletons to Rerun (overlay-on-video and skeleton-only views).
- Parameters:
pose_seq – The sequence to log.
source_video – If given, downscaled frames are logged under
video/as a backdrop.save – Write a standalone
.rrdrecording to this path.spawn – Launch the Rerun viewer.
blank_canvas – Also log a skeleton-only view under
skeleton/.frame_scale – Downscale factor for logged video frames (keypoints scaled to match).
conf_thresh – Hide keypoints below this confidence.
entity_prefix – Prefix all entity paths (use distinct prefixes to compare two sequences side-by-side in one recording, e.g.
"demoA/","demoB/").
- kodokan.make_problem(catalog: dict, target_key: str, *, mode: str = 'video_to_name', n_choices: int = 4, similarity=None, rng: Random | None = None, clip_for=None) Problem[source]
Build a multiple-choice problem for
target_keyfrom a techniquecatalog.catalogmapstechnique_key -> {"name": str, "clips": [clip dicts]}.clip_for(key)returns a looping clip for a key (default: first clip); forname_to_videothe choices are clips, forvideo_to_namethey are names.
- kodokan.next_target(keys, history, *, focus=None, rng: Random | None = None) str[source]
Pick the next technique to quiz: unseen and often-missed (within
focus) first.Spaced-repetition-ish weight = (wrongs + 1) / (seen + 1): unseen -> 1, frequently wrong -> high, mastered -> low.
historyis a list of response dicts.
- kodokan.pose_store(directory: str | Path | None = None)[source]
A dict-like
{video_id: PoseSequence}store backed by per-clip Parquet.
- kodokan.render_skeleton_video(pose_seq: PoseSequence, *, out_path: str | Path, source_video: str | Path | None = None, blank_canvas: bool = False, conf_thresh: float = 0.3, fps: float | None = None, person_colors=((0, 255, 0), (255, 0, 255), (255, 255, 0), (0, 165, 255)), background=(0, 0, 0), thickness: int = 2, radius: int = 4) Path[source]
Render skeletons to an MP4.
- Parameters:
pose_seq – The pose sequence to draw.
out_path – Output
.mp4path.source_video – Source clip; required (and used as backdrop) unless
blank_canvas=True.blank_canvas – If True, draw on a solid
backgroundinstead of the video.conf_thresh – Hide keypoints/edges below this confidence.
fps – Output fps (defaults to the sequence’s source fps).
person_colors – BGR colors per person slot.
background – BGR background color for the blank canvas.
thickness – Edge thickness / joint radius in pixels.
radius – Edge thickness / joint radius in pixels.
- Returns:
The output path.
- kodokan.score(query_features: ndarray, reference: dict) dict[source]
Score a query against a reference, calibrated to the genuine-demo spread.
scoreis 0–100: 100 ≈ as close as the closest genuine demo, 0 ≈ as far as the 90th-percentile (or worse).closer_than_pctis the percent of genuine demos the query is closer-to-reference than.
- kodokan.score_response(correct_key: str, chosen_key: str, *, similarity=None, max_partial: float = 0.5) float[source]
Confusion-weighted credit in [0, 1].
Correct = 1.0. A wrong answer earns partial credit proportional to how confusable the chosen technique is with the correct one (an honest mistake between look-alike throws is penalized less than confusing two obviously different throws).
- kodokan.segment_demonstrations(pose_seq: PoseSequence, *, video_path: str | Path | None = None, use_optical_flow: bool = False, min_two_person_frac: float = 0.0, **find_kwargs) list[Segment][source]
Find demonstrations, annotate each with its two-person coverage, and gate.
Uses pose motion energy by default (fuse optical flow with
use_optical_flow). Segments whose two-person coverage is belowmin_two_person_fracare dropped (a throw demonstration needs both tori and uke), then re-indexed in time order.