Skip to content

Add pose estimation keypoint preprocessing to Sapiens2ImageProcessor - #47199

Open
Sainava wants to merge 5 commits into
huggingface:mainfrom
Sainava:feat/sapiens2-pose-pipeline
Open

Add pose estimation keypoint preprocessing to Sapiens2ImageProcessor#47199
Sainava wants to merge 5 commits into
huggingface:mainfrom
Sainava:feat/sapiens2-pose-pipeline

Conversation

@Sainava

@Sainava Sainava commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

CI

What does this PR do?

As discussed with @guarin in the previous Sapiens2 loss function PR (#46764), this PR implements the preprocessing data pipeline required for Sapiens2 pose estimation fine-tuning.

Specifically, it updates Sapiens2ImageProcessor (_preprocess_image_like_inputs) to handle raw keypoint coordinates. It implements:

  1. Vectorized 2D Gaussian Heatmap Generation: Transforms raw (x, y) image coordinates into scaled (H, W) heatmap targets using inverse affine transformations based on the cropped bounding boxes.
  2. Visibility Masking: Handles COCO-style visibility flags, zeroing out weights for invisible or out-of-bounds keypoints.
  3. Memory-Optimized Weight Tensors: Formats the label_weights to (num_kps, 1, 1) to utilize PyTorch broadcasting during loss calculation, avoiding dense (num_kps, H, W) allocations.

Testing

I ran the full integration test suite for the Sapiens2 Image Processor locally to verify all shapes, visibility masking, and exceptions function as expected.
Command run: pytest tests/models/sapiens2/test_image_processing_sapiens2.py
Results: 22 passed, 7 skipped (All passing)

  • I confirm that this is not a pure code agent PR.

Before submitting

  • This PR fixes a typo or improves the docs (you can dismiss the other checks if that's the case).
  • Did you read the contributor guideline and the
    Pull Request checks?
  • Was this discussed/approved via a Github issue or the forum? Please add a link
    to it if that's the case.
  • Did you make sure to update the documentation with your changes according to the guidelines?
  • Did you write any new necessary tests?

Who can review?

Hi @guarin , following up on our chat in #46764! Here is the initial implementation for the Sapiens2 preprocessing pipeline we discussed.

@guarin guarin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey thanks a lot for the PR! This looks already pretty good, all the important pieces are here. Left some comments regarding naming, structure, and tests :)

Comment thread src/transformers/models/sapiens2/image_processing_sapiens2.py Outdated
Comment thread src/transformers/models/sapiens2/image_processing_sapiens2.py
Comment thread src/transformers/models/sapiens2/modular_sapiens2.py Outdated
Comment thread src/transformers/models/sapiens2/modular_sapiens2.py
Comment thread src/transformers/models/sapiens2/modular_sapiens2.py Outdated
center = centers[person_idx]
scale = scales[person_idx]

hm_coords = (raw_coords - center + 0.5 * scale) / scale * heatmap_size

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
hm_coords = (raw_coords - center + 0.5 * scale) / scale * heatmap_size
hm_coords = ((raw_coords - center) / scale + 0.5) * heatmap_size

Comment on lines +527 to +528
person_kps = image_keypoints[person_idx]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
person_kps = image_keypoints[person_idx]
person_keypoints = image_keypoints[person_idx]

weights_list.append(torch.zeros((0, heatmap_h, heatmap_w), dtype=torch.float32))
continue

person_kps_tensor = torch.tensor(person_kps, dtype=torch.float32)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We prefer to not use abbreviations

Suggested change
person_kps_tensor = torch.tensor(person_kps, dtype=torch.float32)
person_keypoints_tensor = torch.tensor(person_kps, dtype=torch.float32)

Same for other variables below

center = centers[person_idx]
scale = scales[person_idx]

hm_coords = (raw_coords - center + 0.5 * scale) / scale * heatmap_size

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
hm_coords = (raw_coords - center + 0.5 * scale) / scale * heatmap_size
heatmap_coords = (raw_coords - center + 0.5 * scale) / scale * heatmap_size

Comment on lines +505 to +506
heatmap_h = self.size["height"] // 4
heatmap_w = self.size["width"] // 4

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's pack the whole heatmap generation logic into a generate_upd_gaussian_heatmaps function. Could you also add some tests to test_processing_sapiens2.py that assert that the generated outputs match outputs from the original implementation in https://github.com/facebookresearch/sapiens2/blob/7e5bae88456ac418ff0e58e74106c9fe192055d4/sapiens/pose/src/datasets/codecs/utils/gaussian_heatmap.py#L155?

Something that seems suspicious is that the original uses a np.maximum that is missing from our implementation (might not be necessary): https://github.com/facebookresearch/sapiens2/blob/7e5bae88456ac418ff0e58e74106c9fe192055d4/sapiens/pose/src/datasets/codecs/utils/gaussian_heatmap.py#L229

@HuggingFaceDocBuilderDev

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

@github-actions

Copy link
Copy Markdown
Contributor

[For maintainers] Suggested jobs to run (before merge)

run-slow: sapiens2

@github-actions

Copy link
Copy Markdown
Contributor

CI recap

Dashboard: View test results in Grafana
Latest run: 29166551086:2
Result: success | Jobs: 5 | Tests: 317 | Failures: 7 | Duration: 4m 8s

@Sainava

Sainava commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

Hey @guarin,

I've implemented the changes.

Regarding the np.maximum : The original codebase uses a single global (K, H, W) tensor for all people, which requires np.maximum to prevent overlapping keypoints from summing past 1.0. Because our implementation creates isolated (num_kps, H, W) tensors per individual person crop, cross-person overlap is not possible, making np.maximum unnecessary here I think.

Let me know if anything else is needed :)

@guarin guarin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey thanks for the updates! Sorry for the delay, was off for a while. I left some comments, my biggest concern is that the current implementation might not match the original one from sapiens2.

Comment on lines +504 to +505
keypoint_heatmap_downscale_factor: int = 4,
keypoint_heatmap_sigma: float = 6.0,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
keypoint_heatmap_downscale_factor: int = 4,
keypoint_heatmap_sigma: float = 6.0,

Not needed as they are already in **kwargs. Please add the default values above like this:

class Sapiens2ImageProcessor(BeitImageProcessor):
    valid_kwargs = Sapiens2ImageProcessorKwargs
    ...
    keypoint_heatmap_downscale_factor = 4
    keypoint_heatmap_sigma = 6.0

Comment on lines +526 to +527
kwargs["keypoint_heatmap_downscale_factor"] = keypoint_heatmap_downscale_factor
kwargs["keypoint_heatmap_sigma"] = keypoint_heatmap_sigma

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
kwargs["keypoint_heatmap_downscale_factor"] = keypoint_heatmap_downscale_factor
kwargs["keypoint_heatmap_sigma"] = keypoint_heatmap_sigma

Comment on lines +538 to 539
input_data_format: ChannelDimension | None,
return_tensors: str | TensorType | None,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure input_data_format can be None here? It is usually only ChannelDimension in the codebase.

Comment on lines +552 to +553
data["pixel_values"] = self._preprocess(images, **images_kwargs)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
data["pixel_values"] = self._preprocess(images, **images_kwargs)
data["pixel_values"] = self._preprocess(images, **images_kwargs, boxes=boxes)

Comment on lines +566 to +569
processed_segmentation_maps = self._preprocess(
images=processed_segmentation_maps, **segmentation_maps_kwargs
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
processed_segmentation_maps = self._preprocess(
images=processed_segmentation_maps, **segmentation_maps_kwargs
)
processed_segmentation_maps = self._preprocess(
images=processed_segmentation_maps, **segmentation_maps_kwargs, boxes=boxes
)

Comment on lines +346 to +347
dist_sq = (grid_x.unsqueeze(0) - xs) ** 2 + (grid_y.unsqueeze(0) - ys) ** 2
person_heatmaps = torch.exp(-dist_sq / (2 * (sigma**2)))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
dist_sq = (grid_x.unsqueeze(0) - xs) ** 2 + (grid_y.unsqueeze(0) - ys) ** 2
person_heatmaps = torch.exp(-dist_sq / (2 * (sigma**2)))
distance_squared = (grid_x.unsqueeze(0) - xs) ** 2 + (grid_y.unsqueeze(0) - ys) ** 2
person_heatmaps = torch.exp(-distance_squared / (2 * (sigma**2)))

Comment on lines +417 to +418
"""Generates UDP Gaussian heatmaps and visibility weights from raw keypoint coordinates."""
heatmap_height = output_size[0] // downscale_factor

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please document the shape and content of the returned lists


valid_mask = mask * (~out_of_bounds).float()
person_heatmaps = person_heatmaps * valid_mask
person_weights = valid_mask.expand_as(person_heatmaps)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I think this is missing the 3 * sigma bounds from the original code. All weights that are outside of a 3 * sigma radius from the keypoint center must be set to 0: https://github.com/facebookresearch/sapiens2/blob/7e5bae88456ac418ff0e58e74106c9fe192055d4/sapiens/pose/src/datasets/codecs/utils/gaussian_heatmap.py#L204-L206

Comment on lines +330 to +332
weights_list.append(
torch.zeros((0, heatmap_height, heatmap_width), dtype=torch.float32, device=device)
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this shape correct? Shouldn't the weights have one entry per keypoint instead of one entry for each heatmap pixel?

Comment on lines +296 to +304
def test_generate_udp_gaussian_heatmaps_parity(self):
import numpy as np
import torch

from transformers.models.sapiens2.image_processing_sapiens2 import (
box_xywh_to_cxcywh,
boxes_to_crop_params,
generate_udp_gaussian_heatmaps,
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the current test is not ideal as it basically re-implements the logic from the tested function. Could you instead copy the full original function from sapiens2 and write a test that checks that the output from our function matches the output of the original function?

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.

3 participants