Doing It Right with FiftyOne: Sim-to-Real Gesture Classification for Human-Robot Interaction

Sep 16, 2026
16 min read
In 2024 I trained a gesture classifier for human-robot interaction. It scored 24% accuracy, only a hair above the 14.3% chance floor. It lived in a Jupyter notebook so disorganized that I couldn't tell you why the model was bad. I rebuilt it in FiftyOne. It now scores 51.3% on 840 clips of my own robot footage that it never trained on, against a 2024 number that included that footage in training.

This isn’t a story about building models, though. Model architecture changes barely moved the needle. Everything that did was a data problem I could only see once the data was somewhere I could look at it.
Some key takeaways from this project:
  • ~200 real clips beat ~2,000 synthetic ones. On real robot test footage, a gesture classifier trained on 204 real clips scores .760 accuracy against .590 for one trained on 1,980 synthetic clips. In-domain data gets in-domain performance.
  • Data changes moved the needle, model architecture did not. In this sim-to-real gesture classifier, pose normalization added +.17, windowing added +.17, real training data added +.17, and longer windows added +.069. Swapping LSTM for a temporal convolutional network was worth +.005, less than the .017 spread from re-running one model at a different random seed.
  • The sim-to-real gap is not one gap, it is five. This project found feature-space separation, apparent person scale, a labeling convention mismatch between two annotating teams (roughly 40% of the apparent gap), the pose estimator's perspective sensitivity, and a residual 14-point performance gap.
  • Aggregate classification metrics hide per-class nuance. There is no "51% accurate gesture model" here. Three gestures are essentially solved, three sit at or below the 14.3% chance floor, and the same bimodality shows up in Steps 3 and 5.
  • Zero-shot on in-the-wild robot footage more than doubled. The rebuilt classifier scores 51.3% on 840 clips of the author's own robot footage it never trained on, against 24% in the 2024 version that had that footage in its training set.

Why gesture classification for human-robot interaction?

As robots are continuing to operate with and among humans, the need for robots to accurately recognize human intent is more important than ever. But the catch is that the typical pedestrian doesn’t carry a robot controller–so a robot in the wild must perceive humans using only onboard robot sensors (exteroception).
HRI in the wild is a real challenge which I explored as part of my dissertation research. The gesture recognition model was a key part of the exteroceptive system I built. But the implementation itself wasn’t very robust: an LSTM implemented in a Jupyter notebook I got from a labmate, trained on synthetic and real data from the Robot Control Gestures (RoCoG v2) dataset, plus a dataset that I recorded on my actual robot.
Both RoCoG and my dataset used the same, standard set of hand and arm signals:
When I submitted it for publication, the reviewers understandably noticed that the gesture classifier and the overall experiments needed improvement. To paraphrase:
  • gesture classification results are around random chance level…
  • Does gesture recognition accuracy improve in different scenes?
Worse yet, the experiment notebook was so disorganized that I had no idea why the model was performing poorly.
Fast forward to today: I have access to FiftyOne Enterprise (perks of the job!) and can perform a more rigorous inspection at each stage model development.

Step 1: Loading the RoCoG-v2 data

I used the RoCoG v2 dataset for the gesture classifier, a set of 7 standard hand & arm signals. The dataset is free, available online, and includes over 100k annotated videos across synthetic, real, aerial, and ground operating domains. The authors include nominal train, val, and test splits.
Loading the samples into FiftyOne is straightforward (video samples are natively supported) and the gesture labels are applied to each video as fiftyone.Classifications. I added a domain field to specify the source of the data (synthetic-ground / synthetic-air / real-ground / real-air). The authors also included train/val/test designators, which I include in the split field.
This is a large dataset, so I stored it in my cloud bucket and served it using FiftyOne Enterprise’s native cloud support.
From there, taking inventory of the data and managing splits was just a few clicks in the sidebar.
One quick note: I only used about 5% of the synthetic data, taken by randomly sampling the full synthetic split with the Take method. Specifically, I took 4820 synthetic clips (exactly 10x the 482 real clips), for a total of 4820 synth + 482 real = 5302 clips. The 5302 clips span the entire domain and class strata. The smaller dataset keeps things tractable for the compute-heavy operations that come next.
In total, the samples in my dataset are distributed as follows:
The 5,302-clip RoCoG-v2 subset used in this project, distributed across four domains. Synthetic data carries train and val splits, real data carries train and test, and real-ground contributes only 204 training clips. Missing synthetic-ground media reduces the usable counts to 1,980 train and 413 val.
The 5,302-clip RoCoG-v2 subset used in this project, distributed across four domains. Synthetic data carries train and val splits, real data carries train and test, and real-ground contributes only 204 training clips. Missing synthetic-ground media reduces the usable counts to 1,980 train and 413 val.
DomainValTestAll
synthetic-ground415*(413)-2408
synthetic-air412-2412
synthetic (total)827-4820
real-ground-100304
real-air-901178
real (total)-191482
all8271915302
There are a total of four domains (synthetic-ground, synthetic-air, real-ground, real-air); synthetic data has train and val splits, while real data has train and test splits. I only train and evaluate on ground samples for the causal model, so data are ground unless specified otherwise.

*On top of that, some of the synthetic-ground clips were missing but still contained ground truth data; so technically, only 1980 are used for training and 413 for validation.
This is getting a little complicated so I’d like to clarify the naming convention I use from here on out:
Split names used in this post mapped to the names the RoCoG-v2 authors use, with ground-clip counts for each. Everything after Step 4 is ground only.
Split names used in this post mapped to the names the RoCoG-v2 authors use, with ground-clip counts for each. Everything after Step 4 is ground only.
Split name used in this paperNumber of ground clips
sim-train1980
sim-val413
real-train204
real-test100
All splits are ground-only unless explicitly stated otherwise. Absolutely everything after step 4 is ground only.

Step 2: Adding pose prelabels and embeddings

Right away there’s a pretty big issue. I have plenty of video clips, but my LSTM architecture doesn’t operate on pixels–it needs pose features, and everything downstream depends on them.
I applied YOLO11-pose to all 5302 videos using a custom FiftyOne SDK Python script. The pose keypoints are applied as fiftyone.Keypoints annotations, with the skeleton schema added as a fiftyone.KeypointSkeleton to graphically depict the skeletal poses on each video. Code snippets are provided in the Appendix below.
Next, as a sanity check I computed visual embeddings for 977 clips and pose keypoint embeddings for 566 clips so that I could inspect them in FiftyOne’s embeddings panel. The embeddings panel is a great tool to quickly identify clusters and outliers.
UMAP plot of 4,612 clip embeddings colored by domain, where synthetic-ground and synthetic-air points spread across the space while real-ground and real-air points cluster to one side.
Qualitatively, we can see that the real samples occupy one small area of the vector space, and the synthetic samples span a much larger vector space. The intersection of these two vector spaces appears pretty minimal. This is true for both the visual features and the pose-keypoint features.
For example, there is this real aerial sample in which the terrain kind of looks like the synthetic ground terrain:
It’s not definitive proof of anything at this point, but it is another indicator of what we already know: the sim and real visuals and gestures are largely distinct. Does this matter for model performance? We’ll find out.

Step 2.5: Normalizing the pose features

At this point, I actually ran a preliminary gesture classifier, which scored 44% accuracy on sim-val.
But this also surfaced another key issue: variance in skeletal pose sizes. The pose keypoints and joints get normalized to the image frame size. Camera distance and body orientation can vary the apparent skeleton scale by up to 2.8× across the ground clips. A torso turned to the side shortens the shoulders exactly like extra distance does.
To address this, I normalized the features to the skeleton itself; you can move the hip to the origin, and scale all the lengths by the shoulder width. camera distance and body orientation together.
This normalization step yielded the single largest performance improvement in this project--44% to 61% on the sim validation split--and I’ll discuss it in the next section. In any event, we have consistent features! Now, time to really train the classifier.

Step 3: Classifying the gesture video clips

The first ML task for the dataset, and the one I previously explored, is classifying the gesture clips.
To accomplish this, I used the FiftyOne Python SDK to train a Long Short-Term Memory (LSTM) model on the RoCoG's synthetic training clips (ground + aerial, n=3980), then evaluated it on sim-val and real-test splits. The end goal is to optimize model accuracy on the ground samples in the real-test split, since that’s the domain I’m targeting for my robot.
The LSTM is applied bidirectionally to each frame’s skeletal pose keypoint features for each video, generating an estimate and confidence for each frame. I took the non-background maximum confidence gesture to be the clip’s overall classification, and saved the class and confidence as a fiftyone.Classification.
FiftyOne’s model evaluation panel (MEP) makes it easy to get a rundown of how the clip classifier works on the ground real-test:
FiftyOne model evaluation panel summary for the whole-clip gesture classifier on real-test, showing 0.47 accuracy, precision, recall, and F1 across 100 samples.
The key performance numbers here are 47% accuracy and 39% macro F1 for the LSTM on the ground real-test split. There’s also an interesting breakdown here which we can see using the Class Performance tab:
F1 score per gesture class: Advance 0.667, Attention 0, FollowMe 0.8, Halt 0, MoveForward 0.111, MoveInReverse 0.65, Rally 0.526.
Bar chart of F1 score per gesture class, with three gestures at or near zero and four above 0.5.
Confusion matrix for the whole-clip gesture classifier on real-test, with correct predictions concentrated on Advance, FollowMe, MoveForward, MoveInReverse, and Rally.
  • Advance, FollowMe, MoveInReverse, and Rally all have F1 scores above .5
  • Attention, Halt, and MoveForward have F1 scores at or below ~.1
And for completeness, here is how the LSTM performed on different evaluation splits:
Whole-clip LSTM performance on each evaluation split. The model loses about 14 points of accuracy moving from synthetic validation to real test footage, whether or not aerial clips are included.
Whole-clip LSTM performance on each evaluation split. The model loses about 14 points of accuracy moving from synthetic validation to real test footage, whether or not aerial clips are included.
ScopeClipsAccuracyF1
grounds+aerial827.594.565
ground+aerial191.461.378
ground413.608.584
ground100.470.393

What this tells us

The classifier has 47% accuracy on real clips the model has never seen, against a 14.3% chance floor. The model was trained only on synthetic data, so this is a proper transfer, not overfitting the training data. It also loses about 14 points of accuracy going from synthetic to real.
But the average is misleading: four gestures clear 0.5 F1 and three are near-zero. This isn’t a "47% model", rather it’s a model that knows four gestures well and doesn't know three.
This step also exposed a subtle presentation of the sim-to-real gap: differences in pose conventions between real and synthetic data.

Step 4: Temporal classification with a causal model you can deploy

The clip classifier answers what the gesture is, but the robot also needs to estimate when a gesture occurs. It never receives a tidy .mp4, it receives sequential frames, and has to buffer them to decide on what it has seen so far. So the model has to be causal: a trailing window input, a gesture and a confidence out.
As with any supervised learning task, we need labeled training data for this. But there’s a key limitation of the existing dataset: there are no temporal labels on the real data. We’ll have to use the existing class labels and devise a way to identify the timeframe when the gesture occurs.
FiftyOne’s fiftyone.TemporalDetection label type is designed for exactly this scenario--capturing a class and the temporal duration. With the recent improvements to video annotation, it’s straightforward to view, add, and modify the temporal labels directly in the app.
What followed was a discovery phase that involved lots of trial and error. The key tools here were the FiftyOne visualization, annotation, tags & views to manage data splits, and the Python SDK to run Torch model training and inference. The key underlying pattern was differences in how samples were labeled:
  • Gestures in the synthetic samples start partway through the clip. The beginning and end frames are specified in a separate metadata file.
  • Gestures in the real samples also start partway through the clip. However, the beginning and end frames aren’t specified, only the overall label.
The initial causal models performed poorly because the video frames were using different ground truth labeling conventions. Effectively, the model was learning one temporal window and attempting to classify another. In this case, the differences in labeling conventions and data format accounted for about 40% of the model’s discrepancy on the sim-val split. Fixing the convention brings the sim data in line with the real data and narrowed the gap significantly.

Finding a common representation

Knowing that I have different ground truth conventions in play, any downstream training or evaluation tasks will be useless unless I can apply a unified temporal labeling convention across my data.
I needed something that can be quickly applied to synthetic or real data, from the RoCoG dataset or other sources. So I devised a simple rule-based scheme:
  • The gesture begins when the wrist keypoint goes above the elbow keypoint for the first time in the clip (minus a learned offset*)
  • The gesture ends when the wrist keypoint goes below the elbow keypoint for the last time in the clip (plus a learned offset*)
*For most gestures the offset is minimal ( < .15 seconds), indicating that the offset isn’t really needed – the rules work well on their own.
Gestures in the dataset clips are very conspicuous, and we can exploit this knowledge--no one raises their hand above their elbow unless a gesture happens. This rules out false positives from people swinging their arms while swaying in the real splits.
After the correction, all the RoCoG data has a shorter, more consistent segment of the video annotated.
Quick note: I excluded aerial samples for further analysis, because the rule above breaks down on aerial samples. Even after normalizing the pose keypoints to align along the skeletal torso axis instead of the aerial video frame axis, there is apparent shortening of the forearms by about 25%. Because I’m using this rule to add ground truth labels, I made the choice to exclude the aerial split. The ultimate goal here is a gesture classifier for a ground robot, so this seemed prudent to keep the rest of the project scoped and tractable.
Apparent shortening of the forearm when viewed from aerial perspective
To validate the automated rule-based approach, I annotated a validation subset of the real and synthetic samples. The rule-based approach had a .970 median IoU against my hand-annotated samples, and a .734 IoU against the full synthetic window with clearly defined lead and trail buffers. So there is excellent overlap between the rule and my hand-annotated samples; the rule can consistently and realistically extract the window when the gesture is happening, so I’ll use it to apply the ground truth temporal windows on the samples.
And for bookkeeping purposes, I added a gt_source field to ensure that the original RoCoG TemporalDetections didn’t get mixed and matched against my own rule-based TemporalDetections.
To conclude: we now have consistent ground truth labels and gesture convention across the entire RoCoG dataset. This came after identifying a difference in how the synthetic and real sample ground truth windows were labeled, and replacing both with a rule-based approach which matched my hand-annotated method.
With ample training data in hand, I can move on to model training and evaluation.

Step 5: Model training and evaluation

I trained and evaluated two model architectures: LSTM and Temporal Convolutional Network (TCN), both commonly used for classifying short temporal sequences. Since both can accommodate an arbitrarily long input sequence, I used windows of .5, 1.0, and 2.0 seconds.
I used the FiftyOne Python SDK to select the relevant training and eval splits, and then train the LSTM and TCN implementations in PyTorch. I then applied the trained model back onto my FiftyOne samples using the apply_model() function.
I can also evaluate them natively within FiftyOne, again using the evaluation module. For temporal detections on video clips, FiftyOne supports ActivityNet-style evaluation. ActivityNet compares the classwise predictions as well as the IoU of the ground truth and predicted TemporalDetection windows.
Window length affects model accuracy more than model architecture or random seed spread
Clip accuracy for the LSTM and TCN gesture classifiers across training sets. The bottom row is the asymmetry: 204 real clips reach .760 on real-test while collapsing to .237 on synthetic, whereas 1,980 synthetic clips transfer partway to both domains.
Clip accuracy for the LSTM and TCN gesture classifiers across training sets. The bottom row is the asymmetry: 204 real clips reach .760 on real-test while collapsing to .237 on synthetic, whereas 1,980 synthetic clips transfer partway to both domains.
ModelAccuracy: sim-val (413)Accuracy: real-test (100)sim-> real gap
LSTM, whole clip (Step 3).608.470+ .138
LSTM, 2s window.964.640+ .324
LSTM, 2s window.949.810+ .139
TCN, 2s window.944.590+ .354
TCN, 2s window.944.820+ .124
TCN, 2s window.237.760- .523
And macro F1:
Macro F1 for the same runs, which tells the same story as accuracy. Adding 204 real clips to the synthetic training set narrows the sim-to-real gap from roughly .35 to roughly .13 for both architectures.
Macro F1 for the same runs, which tells the same story as accuracy. Adding 204 real clips to the synthetic training set narrows the sim-to-real gap from roughly .35 to roughly .13 for both architectures.
ModelF1: sim-val (413)F1: real-test (100)sim-> real gap
LSTM, whole clip (Step 3).584.393+ .191
LSTM, 2s window.964.615+ .349
LSTM, 2s window.949.813+ .136
TCN, 2s window.945.540+ .405
TCN, 2s window.944.817+ .127
TCN, 2s window.224.743- .519
And here’s the per-gesture model F1 on real-test for different training sets:
Per-gesture F1 on real-test across six training configurations. FollowMe is solved in every run, Halt never clears .73, and adding real training data lifts per-class performance far more than switching architecture does.
And lastly, the mAP table which shows temporal localization performance by training and eval sets:
Temporal localization measured as mean average precision. On real-test, 204 real clips (.2517) essentially tie 1,980 synthetic plus 204 real (.2542), and both roughly double synthetic alone (.1497). At this scale the real clips drive the performance.
Temporal localization measured as mean average precision. On real-test, 204 real clips (.2517) essentially tie 1,980 synthetic plus 204 real (.2542), and both roughly double synthetic alone (.1497). At this scale the real clips drive the performance.
Training setreal-test (100)
synthetic (TCN).1497
synthetic (LSTM).1712
synthetic+real (TCN).2542
real only (TCN).2517
*mAP values cannot be computed if there are no valid temporal predictions.
There are some key takeaways here:
  • Adding real data to the training set gives a minor degradation to the synthetic evaluation metrics, but a ~20 point improvement to eval metrics on the real dataset
    • The bottom row reveals an asymmetry between sim and real data: training on 204 real clips transfers to real and not at all to synthetic; training on 1,980 synthetic clips transfers partway to both.
  • Window length is the most important model parameter. Across both LSTM and TCN architectures, accuracy improves monotonically by ~7 percentage points as the temporal window length is extended from 0.5 to 2.0 seconds.
  • Model architecture is significantly less important; accuracy varies by about .005 between different model architectures, less than the ~.017 variance from choosing a different random seed.
  • When trained on synthetic+real, the TCN (2.0s window) model’s clip classification accuracy on real-test is 82%, and the LSTM’s (2.0s window) accuracy is 81%; both modest improvements over the 73% reported by the RoCoG paper’s authors.
  • Models trained on real-train (n=204) outscored models trained on sim-train (n=1980) when evaluated on real-test (n=100).
  • The average values hide the per-class discrepancies: adding real training data improves per-class performance, not new model architectures.
  • Temporal evaluation in the last table is particularly eye-opening: 204 real training clips (0.2517) essentially tie 1,980 synthetic + 204 real (0.2542), and both beat synthetic alone (0.1497). At this scale the real clips drive the performance.
If there’s a common theme here, it’s that model performance varies widely based on the inputs and the largest variance in model performance comes from the data itself. FiftyOne allowed me to systematically find which variables were important for model performance.
Finally, the moment of truth. How does the model, trained on open-source data, perform on my robot when I give it gestures in the wild?

Step 6: Zero-shot evaluation on in-the-wild robot data

I recorded my own version of the gesture dataset in the wild. In total, my in-the-wild dataset had all 7 gestures, recorded across 4 different scenes, 3 different roles, recorded with/without verbal commands, and 5 repetitions each. In total, I have 7*4*3*2*5 = 840 clips that the model has not seen at all.
Each clip had ground truth labels but no temporal window. I applied the “wrist over elbow” rule from earlier to generate the temporal labels, and validated them against a few manually-annotated samples to ensure there was good IoU overlap. The “wrist over elbow” rule matched my manual annotations at .81 median IoU, so I then applied the rule to the remainder of the 840 samples of the in-the-wild dataset.
A person performing a gesture in a building lobby, with pose keypoints detected in green on them and on bystanders in the background.
One quick note: RoCoG data is sampled at ~30 frames per second, while my robot camera captured images at ~17 FPS.
Finally, I applied the trained causal models to the in-the-wild dataset and ran an ActivityNet-style eval*.
*One quick note about ActivityNet evaluation: the mAP isn’t computed over all 840 samples, only the ones with valid scorable temporal window (n=719)
Here’s how the final models performed on my in-the-wild dataset:
Zero-shot results on 840 clips of in-the-wild robot footage that none of the models had seen. The LSTM with a 2-second window trained on synthetic plus real reaches 51.3% clip accuracy, more than double the 24% from the 2024 version that had this footage in its training set.
Zero-shot results on 840 clips of in-the-wild robot footage that none of the models had seen. The LSTM with a 2-second window trained on synthetic plus real reaches 51.3% clip accuracy, more than double the 24% from the 2024 version that had this footage in its training set.
ModelClip accuracyTemporal mAP
LSTM, 2s window.448.207
LSTM, 2s window.513.229
TCN, 2s window.471.222
And here’s the per-gesture breakdown for the LSTM, 2s model trained on synthetic+real:
Per-gesture clip accuracy and temporal mAP for the best in-the-wild model, the LSTM with a 2-second window trained on synthetic plus real. FollowMe, MoveForward, and Rally are near-perfect with temporal mAP above .5, while Attention, Halt, and MoveInReverse sit at or below the 14.3% chance floor.
Per-gesture clip accuracy and temporal mAP for the best in-the-wild model, the LSTM with a 2-second window trained on synthetic plus real. FollowMe, MoveForward, and Rally are near-perfect with temporal mAP above .5, while Attention, Halt, and MoveInReverse sit at or below the 14.3% chance floor.
GestureTemporal mAP
Advance[not run]
Attention.026
FollowMe.784
Halt.000
MoveForward.621
MoveInReverse.000
Rally.510
Key takeaways:
  • Headline accuracy improved from 24% in my 2024 paper to 51.3% in this iteration, a > 2X improvement. Notable because the 2024 version included the in-the-wild data in training, and this time it trained only on RoCoG.
  • Once again, the per-class table and eval runs reveal nuance around the overall metrics:
    • Advance has no valid temporal predictions in this split - hence the mAP value could not be computed.
    • FollowMe, MoveForward and Rally are near-perfectly accurate and have temporal mAP > 0.5
    • Attention, Halt, and MoveInReverse are effectively impossible to classify or localize temporally - at or below chance level.
The model is highly dependent on the specific class/gesture, and temporal detection and classification are two separate metrics entirely.
Still, this is a massive headline improvement over what I did before, and the additional information from each phase provided more insight than I could have ever gleaned from my Jupyter notebook in 2024.

What actually closed the sim-to-real gap

I’ve talked a lot about different phases of model training, and have gone on some interesting tangents. But to summarize, here’s what actually moved the needle on model performance:
Every change that measurably moved the sim-to-real gesture classifier, with the metric each was measured against. Four of the six gains came from data and feature decisions rather than architecture, and unifying the labeling convention lowered temporal mAP because it corrected the evaluation criteria, not because the model got worse.
Every change that measurably moved the sim-to-real gesture classifier, with the metric each was measured against. Four of the six gains came from data and feature decisions rather than architecture, and unifying the labeling convention lowered temporal mAP because it corrected the evaluation criteria, not because the model got worse.
ChangeBeforeAfterDelta
Normalize pose keypoints to skeleton, not image frame.44.61+ .17
Whole-clip LSTM → causal 2s sliding window model (synthetic training data only).47.64+ .17
Add 204 real clips to 1,980 synthetic training clips.64.81+ .17
Extend causal model window from 0.5s to 2.0s (LSTM and TCN).861.930+ .069
Unify labeling convention across domains.470.287- .183
Zero-shot performance on in-the-wild dataset (840 never-before-seen clips).240.513+ .273
So in total, we’ve quantified the effects of feature normalization, model architecture, and sim/real training data on model performance. We also see that unifying the labeling convention results in an apparent degradation; note that this isn’t a change in model performance, but a change in its evaluation criteria that was overlooked before I had a chance to investigate with FiftyOne.
These allowed me to make a substantial performance improvement over my 2024 model.

References

If you’re interested in the RoCoG dataset, see the paper:
A. V. Reddy et al., “Synthetic-to-Real Domain Adaptation for Action Recognition: A Dataset and Baseline Performances,” in IEEE International Conference on Robotics and Automation (ICRA), 2023.
The RoCoG GitHub repo can be found here: https://github.com/reddyav1/RoCoG-v2
Both are used under CC by 4.0.
Here is the paper for my original gesture classifier:
Duncan, John A. et al., “C2HI: Towards a Command and Control Hierarchical Interface for Human-Robot Teams in Dynamic Social Environments,” in 2025 IEEE Engineering Reliable Autonomous Systems (ERAS), 2025.

Appendix: YOLO-pose prelabeling code snippets

import cv2
import fiftyone as fo
import fiftyone.core.storage as fos
from ultralytics import YOLO

model = YOLO("yolo11n-pose.pt")

for sample in dataset.iter_samples(autosave=True, progress=True):
    # Pull locally if media stored in the cloud
    with fos.LocalFile(sample.filepath, mode="r") as local_path:
        cap = cv2.VideoCapture(local_path)
        frame_number = 0
        while True:
            ok, frame = cap.read()
            if not ok:
                break
            frame_number += 1
            h, w = frame.shape[:2]

            result = model(frame, verbose=False)[0]
            if result.keykeypoints.xy) == 0:
                continue

            # Most-confident person: RoCoG clips have exactly one subject.
            i = int(resul
            if float(result.boxes.conf[i]) < 0.25:
                continue

            sample.frames fo.Keypoints(
                keypoints=[fo.Keypoint(
                    label="person",
                    # Fifed to [0, 1]
                    points=[(x / w, y / h)
                            for x, y in result.keypoints.xy[i].tolist()],
                    confidence=result.keypoints.conf[i].tolist(),
                )]
            )
        cap.release()
To turn the 17 points into a skeleton, use this snippet:
dataset.skeletons = {"keypoints": fo.KeypointSkeleton(
    labels=COCO17_LABELS, edges=COCO17_EDGES,
)}
dataset.save()

Questions? We have answers.

Headshot of John Duncan
John Duncan
Machine Learning Engineer
I'm a customer success machine learning engineer here at Voxel51, where I work with our enterprise customers to implement their ML workflows in FiftyOne. I earned my Ph.D. in Robotics from UT-Austin, where I specialized in multimodal perception for command and control of robots in dynamic, unstructured environments.
See all articles by John Duncan

Talk to an AI expert

Loading related posts...