Skip to content

feat(augment): support Perspective on the Kornia backend - #1330

Merged
Borda merged 9 commits into
roboflow:developfrom
adhavan18:feat/kornia-perspective
Aug 17, 2026
Merged

feat(augment): support Perspective on the Kornia backend#1330
Borda merged 9 commits into
roboflow:developfrom
adhavan18:feat/kornia-perspective

Conversation

@adhavan18

Copy link
Copy Markdown
Contributor

What

Adds Perspective to the Kornia GPU augmentation backend. Follow-up to #1277, same issue (#1252).

Why

Perspective is one of the twelve names documented in aug_configs that raise ValueError on Kornia while working on Albumentations, so the same aug_config trains on a CPU box and hard-fails on a GPU box.

It is also the only geometric name from that issue that can be added without changing the output resolution, which is what makes it safe. Measured on 8444ce3:

RandomCrop(32,32)      in=(64,64) out=(32,32)   size changed
CenterCrop(32,32)      in=(64,64) out=(32,32)   size changed
RandomResizedCrop(48)  in=(64,64) out=(48,48)   size changed
RandomPerspective      in=(64,64) out=(64,64)   size preserved

That distinction matters because of on_after_batch_transfer, which rebuilds the batch as:

batch = (NestedTensor(img_aug, samples.mask), targets)

samples.mask is the pre-augmentation padding mask. Any transform that resizes the image leaves the mask describing a different shape, and NestedTensor does not validate the pair:

NestedTensor(img 32x32, mask 64x64) -> constructed, no error

So the three crop names cannot be mapped by adding a registry entry alone; they need the mask cropped in step, which is a larger change with its own design question. This PR does not attempt it, and adds a test pinning that those three stay rejected so the gap is not quietly "completed" later.

Correction to my own earlier note on #1252: I said there that the crops "have Kornia equivalents with matching semantics" and offered to send all four. That was true transform-for-transform and wrong at the pipeline level, which I only found by tracing where the pipeline output goes. Perspective is the only one of the four that is actually shippable as a registry entry.

Design

scale maps onto distortion_scale directly: both express corner displacement as a fraction of the image side.

A (min, max) pair collapses to its upper bound and logs a warning, the same asymmetry GaussNoise already documents (Albumentations samples per call, Kornia takes a single fixed value). A scalar is passed straight through with no warning, since that is an exact request rather than a collapse.

keep_size=False raises rather than being silently dropped. Albumentations defaults it to True and Kornia's RandomPerspective always behaves that way, so the default maps cleanly; the non-default changes the output resolution, which is exactly the case the padding mask cannot survive. Refusing names the reason and points at the CPU backend.

Boxes are handled by the existing AugmentationSequential(data_keys=["input", "bbox_xyxy", ...]), and unpack_boxes already clamps to image bounds and drops zero-area boxes, so no target-handling change is needed here.

Testing

tests/datasets/test_kornia_transforms.py    98 passed
tests/datasets/test_kornia_transforms.py::TestPerspectiveFactory    6 passed

Six new tests. Five fail on the parent commit; the sixth is the guard that the crop names stay rejected, which passes on both sides by design.

test what it pins
test_scale_range_collapses_to_upper_bound_and_warns the divergence from the CPU path is announced
test_scalar_scale_does_not_warn an exact request stays quiet
test_keep_size_false_is_refused_not_ignored the resolution-changing case is refused
test_output_keeps_the_input_resolution the property the whole mapping rests on
test_boxes_follow_the_warp boxes are warped, not left behind
test_crop_names_from_1252_remain_unsupported the crops stay rejected until the mask is handled

Environment: kornia 0.8.3, albumentations 2.0.8, CPU.

tests/datasets/ as a whole did not finish inside my time limit, so I ran the affected module in full rather than the whole directory. Nothing outside kornia_transforms.py is touched, and the change is one new registry entry plus its factory.

Docs

None. The name was already documented in aug_configs as supported; this makes that true for the Kornia backend. Happy to add a note about the range-collapse behaviour if you would like it stated outside the docstring.

@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.65217% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 85%. Comparing base (b18c9ca) to head (573495b).

Additional details and impacted files
@@           Coverage Diff           @@
##           develop   #1330   +/-   ##
=======================================
  Coverage       85%     85%           
=======================================
  Files          111     111           
  Lines        14066   14086   +20     
=======================================
+ Hits         12006   12025   +19     
- Misses        2060    2061    +1     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Borda
Borda requested a balanced review from Copilot August 11, 2026 22:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds Kornia GPU backend support for Perspective.

Changes:

  • Registers a RandomPerspective factory with scale and size handling.
  • Adds tests for warnings, resolution, boxes, and unsupported crops.
  • Review found semantic/API compatibility and documentation issues.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
src/rfdetr/datasets/kornia_transforms.py Adds Kornia Perspective mapping.
tests/datasets/test_kornia_transforms.py Adds Perspective factory tests.
Suppressed comments (4)

tests/datasets/test_kornia_transforms.py:1228

  • Add an explicit -> None return annotation to this new test method.
    def test_scalar_scale_does_not_warn(self):

tests/datasets/test_kornia_transforms.py:1239

  • Add an explicit -> None return annotation to this new test method.
    def test_keep_size_false_is_refused_not_ignored(self):

tests/datasets/test_kornia_transforms.py:1246

  • Add an explicit -> None return annotation to this new test method.
    def test_output_keeps_the_input_resolution(self):

tests/datasets/test_kornia_transforms.py:1258

  • Add an explicit -> None return annotation to this new test method.
    def test_boxes_follow_the_warp(self):

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/rfdetr/datasets/kornia_transforms.py Outdated
Comment thread src/rfdetr/datasets/kornia_transforms.py
Comment thread src/rfdetr/datasets/kornia_transforms.py
Comment thread tests/datasets/test_kornia_transforms.py Outdated
Comment thread tests/datasets/test_kornia_transforms.py Outdated
@adhavan18
adhavan18 force-pushed the feat/kornia-perspective branch from 8dd1ffd to 003a41d Compare August 12, 2026 07:38
@adhavan18

Copy link
Copy Markdown
Contributor Author

rebased onto current develop and pushed 003a41d. the review findings were right on the substance, including the one that says the mapping isn't what i claimed it was, so here's what changed.

the mapping was described wrongly

i wrote that scale "maps onto distortion_scale directly." it doesn't. albumentations treats scale as the standard deviation of a normal and takes abs(N(0, sigma)) per corner; kornia draws uniformly from [0, distortion_scale]. same units, different distributions — small displacements dominate on the CPU path and don't on the GPU path, so the GPU path distorts more on a typical sample even with identical config. there's no value of distortion_scale that makes them equal.

passing the upper bound keeps the worst case roughly aligned, which is the best available, but calling it a parameter rename was wrong. the docstring now says what it actually is, and the warning fires on every Perspective config rather than only on a non-degenerate range, because the divergence doesn't depend on the range being non-degenerate.

a scalar scale was reported wrong

fell out of fixing the above. albumentations normalises a scalar v to (0, v), but _as_range(0.2) returns (0.2, 0.2), so the warning would have printed the wrong CPU-side range. normalised locally with a comment explaining why this one doesn't go through _as_range.

six options were being dropped silently

fit_output, interpolation, mask_interpolation, border_mode, fill, fill_mask had no kornia equivalent and just vanished. every other factory in the file warns on ignored keys (ToGray, Sharpen, Equalize); this one didn't. now it does, following the same idiom.

the docs contradicted the code

aug_configs.py still listed Perspective under "not yet supported on kornia" while the registry supported it. added the table row and the caveat, removed it from the unsupported list. the crops stay unsupported, for the resolution reason in the PR body.

tests

reworked per the review: -> None annotations, the crop-name loop is parameterized so one failure doesn't hide the rest, and the scalar test now asserts the divergence is reported rather than asserting silence, since that was the behaviour i corrected above. added a parameterized case per ignored option.

full CI here will be a better check than my local run — pytest can't collect the module in my environment (transformers is missing BackboneConfigMixin, unrelated to this diff), so i exercised the factory directly and confirmed all 14 assertions.

adhavan18 and others added 5 commits August 13, 2026 09:08
`Perspective` is the one geometric name from roboflow#1252 that maps onto Kornia
without changing the output resolution, which is what makes it safe to add.
Albumentations' `scale` and Kornia's `distortion_scale` are the same quantity
(corner displacement as a fraction of the image side), so it maps directly.

A `(min, max)` pair collapses to its upper bound and logs, the same asymmetry
`GaussNoise` already has: Albumentations samples a fresh value per call,
Kornia takes one fixed value. A scalar stays quiet, since that is an exact
request rather than a collapse.

`keep_size=False` is refused rather than ignored. It changes the output
resolution, and the GPU path in `on_after_batch_transfer` rebuilds the batch
as `NestedTensor(img_aug, samples.mask)`, reusing the pre-augmentation padding
mask. Anything that resizes the image leaves that mask describing a different
shape, and `NestedTensor` does not validate the pair.

That is also why the three crop names from roboflow#1252 are still unsupported: they
resize by construction. A test pins that they stay rejected, so this does not
get "completed" later without dealing with the mask.

Six tests, five of which fail on the parent commit.
…options

The scale mapping was described as direct and it is not: albumentations
treats scale as a sigma and samples abs(N(0, sigma)) per corner, kornia
samples uniformly from [0, distortion_scale], so the GPU path distorts
more on a typical sample. Say so, and warn on every config rather than
only on a non-degenerate range.

Also: a scalar scale is (0, v) in albumentations, not (v, v), so the
reported CPU-side range was wrong; the six unmappable options now warn
instead of vanishing; and aug_configs.py no longer lists Perspective as
unsupported while the registry supports it.
The table row was 134 chars and tripped ruff E501, which is what failed
pre-commit.ci. Also corrects the prose, which still said the divergence
is logged only for a non-degenerate range; it is logged for every config.
@adhavan18
adhavan18 force-pushed the feat/kornia-perspective branch from 2e51da5 to 20b36ef Compare August 13, 2026 03:39
@adhavan18

Copy link
Copy Markdown
Contributor Author

pushed 20b36ef, also rebased onto current develop again.

the pre-commit failure was mine: the Perspective row i added to the table in aug_configs.py was 134 chars against a 120 limit, so ruff E501 tripped. shortened the row and moved the detail into the prose below it.

while fixing that i noticed the prose was stale in a way the earlier push introduced: it still said the divergence is logged for "a range whose ends differ", which was true before but isn't now that the warning fires on every config. corrected.

ruff check and ruff format --check both clean on the changed files now.

@Borda Borda added the enhancement New feature or request label Aug 13, 2026
Borda and others added 4 commits August 15, 2026 22:35
Changes:
- Remove the Kornia Perspective factory and registry entry so unsafe geometric augmentation cannot reach training targets.
- Document Perspective as unsupported on the Kornia backend.
- Replace stochastic Perspective support tests with deterministic rejection coverage for unsupported geometry.

Impact:
- GPU augmentation now fails fast instead of risking stale padding-mask geometry and lossy parameter mapping.
- Users receive accurate backend support documentation; maintainers get reproducible regression coverage.

Verification:
- pre-commit run --all-files: passed.
- Focused unsupported-geometry regression: 4 passed.
- uv run --no-sync pytest tests/datasets/test_kornia_transforms.py -q: 96 passed.
- Full CPU suite: 3958 passed, 70 skipped, 6 failed in unrelated CoreML/ONNX export paths; formal investigation recorded the missing onnxscript dependency and known CoreML/Torch parity instability.
- rtk git diff --check: passed.

Residual limits:
- No CUDA device is available locally.
- The full CPU export gate remains red for non-causal environment/test failures; current CI has not run this commit.
- Strict child-review provenance telemetry is unavailable.

---
Co-authored-by: Codex <codex@openai.com>
Changes:
- Route the detection batch-padding channel through mask-aware Kornia geometry and return its transformed boolean mask in NestedTensor.
- Carry segmentation instance masks and the padding channel together, then split them before target unpacking.
- Keep Perspective fixed-size, document its mapping limits, and add mock plus real seeded regression coverage for boxes, instance masks, and unequal-size batch padding.

Impact:
- GPU geometric augmentation now keeps model padding coordinates aligned with transformed images and labels.
- Perspective remains usable without enabling output-resizing transforms that the fixed-size batch contract cannot represent.

Verification:
- env UV_CACHE_DIR=/private/tmp/rfdetr-perspective-uv uv run --no-sync pytest tests/training/test_module_data.py tests/datasets/test_kornia_transforms.py -q (227 passed).
- run_gates.py lint, format, types, tests, and review gates passed.
- pre-commit run --all-files passed.

Residual limits:
- CUDA integration was unavailable locally.
- The full CPU suite was not rerun because six prior export failures are unrelated to this repair.

---

Co-authored-by: Codex <codex@openai.com>
@Borda
Borda merged commit b2cea23 into roboflow:develop Aug 17, 2026
40 checks passed
@adhavan18

Copy link
Copy Markdown
Contributor Author

thanks @Borda, and thanks for taking the padding-mask half on directly — 573495b fixes the thing I'd flagged as the reason the crop transforms had to stay out.

my read of where that leaves #1252: the blocker was NestedTensor(img_aug, samples.mask) reusing the pre-augmentation mask, so any transform that moved geometry left the mask describing the wrong thing. now that the padding channel rides through the geometry with the image, that objection is gone for the fixed-size transforms. ShiftScaleRotate, ElasticTransform and GridDistortion all keep the output resolution, so they look reachable on the same contract; the three crops still change it and stay out.

happy to pick those up as a follow-up if you want them, or leave it if you'd rather land this first and see how the mask handling behaves in practice.

not touching this branch since you have commits on it — say the word if you want anything rebased or split out.

one small thing on #1350, which is unrelated but overlaps the same file: it's a scalar-vs-range bug in CLAHE where albumentations reads clip_limit=4.0 as (1, 4) and the Kornia path read it as (4, 4), so the default config pinned every sample to maximum contrast. green, and it'll want a trivial rebase once this lands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants