Skip to content

Add ball ground truth emitter and ball positioner - #18

Open
vincenttumminello wants to merge 12 commits into
mainfrom
tumminello/ball-ground-truth-and-shooter
Open

vincenttumminello wants to merge 12 commits into
mainfrom
tumminello/ball-ground-truth-and-shooter

Conversation

@vincenttumminello

Copy link
Copy Markdown
Collaborator

Includes changes from PR #16 so merge that first.

Also includes changes to the camera intrinsics (#17), merge that first.

Can position the ball from a NUbots_K1 message like:

/// Place the NUSim ball and set its velocity (rt/nusim/ball_command)
message NUSimBallCommand {
    enum Frame {
        /// The simulator world {s}
        WORLD = 0;
        /// The yaw-only frame at the torso's ground projection (x forward, z up), i.e. {r}
        ROBOT = 1;
    }
    Frame frame = 1;
    /// Ball centre; a negative z rests it on the floor at its radius
    vec3 position = 2;
    /// Ball centre velocity
    vec3 velocity = 3;
    /// Ball spin; ignored when rolling is set
    vec3 angular_velocity = 4;
    /// Derive the spin for rolling without slipping from the velocity
    bool rolling = 5;
}

which can be used for simulated games or testing goalie behaviour or other tests.

Adds in code to emit the ball ground truth.

vincenttumminello and others added 8 commits September 19, 2026 17:27
Merging main (e4acc8b) spliced a second, older copy of the file into the
middle of publish(), nesting function definitions inside it, so the sdkbridge
module no longer compiled. Restore the pre-merge contents (rt/odom included)
in main's formatting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DyRrq9GsES4TwVCpGM4rLF
The scene keeps the ball body at the world origin and its sphere geom 1.38 m
away, so zero-padded inherited keyframes still put the ball on its spot.
MuJoCo integrates a free body's origin linearly and its rotation separately,
so a spinning ball's centre drifts ~0.5 (w dt)^2 |offset| per step: a ball
rolled at 3 m/s sank into the floor within 50 ms and the contact launched it
to 68 m/s. Kicked balls in NUSim hit the same failure.

SimCore now loads every scene through mjSpec (robots == 1 attaches no
copies) and moves the ball body's origin onto its sphere before compiling,
so the collision bounding volumes stay consistent, then shifts each
keyframe's ball position by the rotated offset so every keyframe still rests
the ball exactly where it did. The supervisor test loads the scene the same
way and checks that no keyframe moved the ball.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DyRrq9GsES4TwVCpGM4rLF
NUSim-only test surface (PROTOCOL.md §6), all nav_msgs/Odometry so no new
IDL on either side:

- rt/nusim/gt/ball, rt/nusim/gt/robot: world-frame pose and twist at the
  50 Hz state cadence, stamped with the wall clock of the physics snapshot,
  for validating NUbots estimators against the simulator.
- rt/nusim/ball_command: place the ball and set its velocity in the world
  or the robot's yaw frame, optionally with the spin for rolling without
  slipping. The Supervisor applies it under the sim mutex, whether or not
  GameController placement is enabled.

The ball's centre velocity is computed from qpos/qvel through the geom
offset (freebody::geom_centre_state), so it stays exact however the body is
laid out. The supervisor test covers placement, the robot frame, rolling
spin, agreement with mj_objectVelocity, and that a rolled ball keeps its
speed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DyRrq9GsES4TwVCpGM4rLF
The real K1 head camera ("Left Camera") is 544x448, rectilinear, with a
normalised focal length of 0.3725: 106.6 deg horizontal, 95.7 deg vertical and
120.2 deg diagonal field of view. NUSim rendered 640x480 at fovy 70 (about
86 deg horizontal), so the robot saw a narrower, differently framed image than
on hardware, which biases anything validated through the vision chain (ball
detection range, how long a ball stays in view). Set the MJCF camera's fovy to
95.7 and the render size to 544x448; the camera now reports focal_length_norm
0.3727 and a 120.2 deg diagonal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DyRrq9GsES4TwVCpGM4rLF
Every visual mesh was near-white (0.92), so when the head looked down to the
side the robot's own white hand and forearm filled the corner of the image and
YOLO took it for a ball: 55% of detections in the ball-localisation benchmark
were this self-detection, and the ball filter locked onto it. The real K1's
arms, legs, feet and head are black; only the chest is light. Colour the arm,
leg, foot and Head_1 meshes dark (as Head_2 already was) and keep the trunk
light. False positives in the benchmark drop to 4.7%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DyRrq9GsES4TwVCpGM4rLF
@jpptm

jpptm commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator

Address conflict + merge w main, I'll review shortly

vincenttumminello and others added 3 commits October 18, 2026 19:35
The keeper stands on the -x goal's centre line, 0.2 m in front of the goal
line and facing up the field, where the mjlab goalkeeper task starts it.
NUbots_K1's tools::GoalieShotBenchmark rolls shots at it from there:
./b run sim/soccer --keyframe goalie.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8eKNzgxTnz2eZSiKYWoZo
The DataReader held the BallCommandReader as its listener until the
participant was torn down, which happens after the reader is destroyed,
so a command arriving during shutdown called into a freed listener.
Delete the DataReader in the destructor instead, which also waits out
any in-flight callback.

on_data_available compared against reader_, but the listener is
registered inside create_datareader, before reader_ is assigned. A
command arriving in that window was left untaken until the next one.
Read from the reader passed to the callback instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants