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); NaN for 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

save_npz(path: str | Path) Path[source]

Persist as a compressed .npz (the dense numeric cache).

times() ndarray[source]

Timestamp (seconds) of each analyzed frame.

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} where baseline is 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 warping path, and the cleaned input arrays a/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") → a PoseSequence with 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 by kodokan[pose]) or "ultralytics" (YOLO11-pose, MPS), which needs kodokan[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 if None.

  • 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, or model_name="yolo11m-pose.pt" for ultralytics).

Returns:

A PoseSequence of 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 PoseSequence whose 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_id column) 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 .rrd recording 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_key from a technique catalog.

catalog maps technique_key -> {"name": str, "clips": [clip dicts]}. clip_for(key) returns a looping clip for a key (default: first clip); for name_to_video the choices are clips, for video_to_name they 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. history is 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 .mp4 path.

  • source_video – Source clip; required (and used as backdrop) unless blank_canvas=True.

  • blank_canvas – If True, draw on a solid background instead 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.

score is 0–100: 100 ≈ as close as the closest genuine demo, 0 ≈ as far as the 90th-percentile (or worse). closer_than_pct is 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 below min_two_person_frac are dropped (a throw demonstration needs both tori and uke), then re-indexed in time order.

kodokan.segments_store(directory: str | Path | None = None)[source]

A dict-like {video_id: <json>} store for demo segments (and metadata).