From 959d763f41b6ba67f1057a2c6c014ef6f8ebbfcc Mon Sep 17 00:00:00 2001 From: Heiner Spiess Date: Tue, 21 Oct 2025 14:56:51 +0200 Subject: [PATCH 1/5] unifying scaling projections, same black border offset for all, fixing documentation --- src/projective/affine.jl | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/src/projective/affine.jl b/src/projective/affine.jl index 846dfc4..abaa6a4 100644 --- a/src/projective/affine.jl +++ b/src/projective/affine.jl @@ -10,7 +10,7 @@ end """ - ScaleRatio(minlengths) <: ProjectiveTransform + ScaleRatio(ratios) <: ProjectiveTransform Scales the aspect ratio """ @@ -18,16 +18,22 @@ struct ScaleRatio{N} <: ProjectiveTransform ratios::NTuple{N} end +# This allows for roundtrip through ScaleFixed and avoids code duplication +fixed_sizes(scale::ScaleRatio, bounds::Bounds) = round.(Int, scale.ratios .* length.(bounds.rs)) -function getprojection(scale::ScaleRatio, bounds; randstate = nothing) - return scaleprojection(scale.ratios) +function getprojection(scale::ScaleRatio{N}, bounds::Bounds{N}; randstate = nothing) where N + return getprojection(ScaleFixed{N}(fixed_sizes(scale, bounds)), bounds; randstate) +end + +function projectionbounds(tfm::ScaleRatio{N}, P, bounds::Bounds{N}; randstate = nothing) where N + projectionbounds(ScaleFixed{N}(fixed_sizes(tfm, bounds)), P, bounds; randstate) end """ ScaleKeepAspect(minlengths) <: ProjectiveTransform -Scales the shortest side of `item` to `minlengths`, keeping the -original aspect ratio. +Scales the sides of `item` to be as least as large as `minlengths`, +keeping the original aspect ratio. ## Examples @@ -42,28 +48,15 @@ struct ScaleKeepAspect{N} <: ProjectiveTransform minlengths::NTuple{N, Int} end +# This allows for roundtrip through ScaleFixed and avoids code duplication +fixed_sizes(scale::ScaleKeepAspect, bounds::Bounds) = round.(Int, maximum(scale.minlengths ./ length.(bounds.rs)) .* length.(bounds.rs)) function getprojection(scale::ScaleKeepAspect{N}, bounds::Bounds{N}; randstate = nothing) where N - # If no scaling needs to be done, return a noop transform - scale.minlengths == length.(bounds.rs) && return IdentityTransformation() - - # Offset `minlengths` by 1 to avoid black border on one side - ratio = maximum((scale.minlengths .+ 1) ./ length.(bounds.rs)) - upperleft = SVector{N, Float32}(minimum.(bounds.rs)) .- 0.5 - P = scaleprojection(Tuple(ratio for _ in 1:N)) - if any(upperleft .!= 0) - P = P ∘ Translation((Float32.(P(upperleft)) .+ 0.5f0)) - end - return P + getprojection(ScaleFixed{N}(fixed_sizes(scale, bounds)), bounds; randstate) end function projectionbounds(tfm::ScaleKeepAspect{N}, P, bounds::Bounds{N}; randstate = nothing) where N - origsz = length.(bounds.rs) - ratio = maximum((tfm.minlengths) ./ origsz) - sz = round.(Int, ratio .* origsz) - bounds_ = transformbounds(bounds, P) - bs_ = offsetcropbounds(sz, bounds_, ntuple(_ -> 0.5, N)) - return bs_ + projectionbounds(ScaleFixed{N}(fixed_sizes(tfm, bounds)), P, bounds; randstate) end """ @@ -80,6 +73,9 @@ end function getprojection(scale::ScaleFixed, bounds::Bounds{N}; randstate = nothing) where N + # If no scaling needs to be done, return a noop transform + (scale.sizes == length.(bounds.rs)) && return IdentityTransformation() + ratios = (scale.sizes .+ 1) ./ length.(bounds.rs) upperleft = SVector{N, Float32}(minimum.(bounds.rs)) .- 1 P = scaleprojection(ratios) From 3dff4a8f2004556b3025b376637e92df86d03cd4 Mon Sep 17 00:00:00 2001 From: Heiner Spiess Date: Tue, 21 Oct 2025 15:49:33 +0200 Subject: [PATCH 2/5] Scale tests include test for black borders and updated bounds test for ScaleRatio --- test/projective/affine.jl | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/projective/affine.jl b/test/projective/affine.jl index 8f62e20..8921fe6 100644 --- a/test/projective/affine.jl +++ b/test/projective/affine.jl @@ -76,6 +76,7 @@ include("../imports.jl") @test_nowarn apply(tfm, keypoints) timage = apply(tfm, image) tkeypoints = apply(tfm, keypoints) + @test !any(isnan.(timage |> itemdata)) @test length.(getbounds(timage).rs) == (25, 25) @test getbounds(timage) == getbounds(tkeypoints) testprojective(tfm) @@ -87,7 +88,8 @@ include("../imports.jl") @test_nowarn apply(tfm, keypoints) timage = apply(tfm, image) tkeypoints = apply(tfm, keypoints) - @test getbounds(timage).rs == (0:25, 0:25) + @test !any(isnan.(timage |> itemdata)) + @test getbounds(timage).rs == (2:26, 2:26) @test getbounds(timage) == getbounds(tkeypoints) testprojective(tfm) end @@ -96,10 +98,14 @@ include("../imports.jl") tfm = ScaleKeepAspect((32, 32)) img = rand(RGB{N0f8}, 64, 96) - @test apply(tfm, Image(img)) |> itemdata |> size == (32, 48) + timg = apply(tfm, Image(img)) |> itemdata + @test timg |> size == (32, 48) + @test !any(isnan.(timg)) img = rand(RGB{N0f8}, 196, 196) - @test apply(tfm, Image(img)) |> itemdata |> size == (32, 32) + timg = apply(tfm, Image(img)) |> itemdata + @test timg |> size == (32, 32) + @test !any(isnan.(timg)) end end From 3f632559fc1660d067966cfa5d7fc1be45858171 Mon Sep 17 00:00:00 2001 From: Heiner Spiess Date: Mon, 27 Oct 2025 12:43:22 +0100 Subject: [PATCH 3/5] update projectionbounds to have nicer bounds and enable composing scaling transformations --- src/projective/affine.jl | 4 ++-- src/projective/compose.jl | 9 ++++++++- test/projective/affine.jl | 16 +++++++++++++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/projective/affine.jl b/src/projective/affine.jl index abaa6a4..51a77ae 100644 --- a/src/projective/affine.jl +++ b/src/projective/affine.jl @@ -75,7 +75,7 @@ end function getprojection(scale::ScaleFixed, bounds::Bounds{N}; randstate = nothing) where N # If no scaling needs to be done, return a noop transform (scale.sizes == length.(bounds.rs)) && return IdentityTransformation() - + ratios = (scale.sizes .+ 1) ./ length.(bounds.rs) upperleft = SVector{N, Float32}(minimum.(bounds.rs)) .- 1 P = scaleprojection(ratios) @@ -88,7 +88,7 @@ end function projectionbounds(tfm::ScaleFixed{N}, P, bounds::Bounds{N}; randstate = nothing) where N bounds_ = transformbounds(bounds, P) - return offsetcropbounds(tfm.sizes, bounds_, ntuple(_ -> 1., N)) + return offsetcropbounds(tfm.sizes, bounds_, ntuple(_ -> 0.5, N)) end """ diff --git a/src/projective/compose.jl b/src/projective/compose.jl index 48b62cf..d8d078f 100644 --- a/src/projective/compose.jl +++ b/src/projective/compose.jl @@ -54,5 +54,12 @@ function getprojection( end function projectionbounds(composed::ComposedProjectiveTransform, P, bounds; randstate = getrandstate(composed)) - return transformbounds(bounds, P) + @assert length(composed.tfms) == length(randstate) + P = CoordinateTransformations.IdentityTransformation() + for (tfm, r) in zip(composed.tfms, randstate) + P_tfm = getprojection(tfm, bounds; randstate = r) + bounds = projectionbounds(tfm, P_tfm, bounds; randstate = r) + P = P_tfm ∘ P + end + return bounds end diff --git a/test/projective/affine.jl b/test/projective/affine.jl index 8921fe6..c92bcf2 100644 --- a/test/projective/affine.jl +++ b/test/projective/affine.jl @@ -82,6 +82,7 @@ include("../imports.jl") testprojective(tfm) end + @testset ExtendedTestSet "ScaleRatio" begin tfm = ScaleRatio((1/2, 1/2)) @test_nowarn apply(tfm, image) @@ -89,7 +90,20 @@ include("../imports.jl") timage = apply(tfm, image) tkeypoints = apply(tfm, keypoints) @test !any(isnan.(timage |> itemdata)) - @test getbounds(timage).rs == (2:26, 2:26) + @test getbounds(timage).rs == (1:25, 1:25) + @test getbounds(timage) == getbounds(tkeypoints) + testprojective(tfm) + end + + @testset ExtendedTestSet "ScaleRatioTwice" begin + tfm = ScaleRatio((4/5, 4/5)) |> ScaleRatio((1/2, 1/2)) + @show getbounds(image).rs + @test_nowarn apply(tfm, image) + @test_nowarn apply(tfm, keypoints) + timage = apply(tfm, image) + tkeypoints = apply(tfm, keypoints) + @test !any(isnan.(timage |> itemdata)) + @test getbounds(timage).rs == (1:20, 1:20) @test getbounds(timage) == getbounds(tkeypoints) testprojective(tfm) end From e1b3a7039ff1b9ab589b741133f6a131fc997cb7 Mon Sep 17 00:00:00 2001 From: Heiner Spiess Date: Tue, 4 Nov 2025 18:34:45 +0100 Subject: [PATCH 4/5] more specific bounds tests --- test/projective/affine.jl | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/test/projective/affine.jl b/test/projective/affine.jl index c92bcf2..cf1647f 100644 --- a/test/projective/affine.jl +++ b/test/projective/affine.jl @@ -77,7 +77,7 @@ include("../imports.jl") timage = apply(tfm, image) tkeypoints = apply(tfm, keypoints) @test !any(isnan.(timage |> itemdata)) - @test length.(getbounds(timage).rs) == (25, 25) + @test getbounds(timage).rs == (1:25, 1:25) @test getbounds(timage) == getbounds(tkeypoints) testprojective(tfm) @@ -97,7 +97,6 @@ include("../imports.jl") @testset ExtendedTestSet "ScaleRatioTwice" begin tfm = ScaleRatio((4/5, 4/5)) |> ScaleRatio((1/2, 1/2)) - @show getbounds(image).rs @test_nowarn apply(tfm, image) @test_nowarn apply(tfm, keypoints) timage = apply(tfm, image) @@ -113,14 +112,14 @@ include("../imports.jl") img = rand(RGB{N0f8}, 64, 96) timg = apply(tfm, Image(img)) |> itemdata - @test timg |> size == (32, 48) - @test !any(isnan.(timg)) - + timg = apply(tfm, Image(img)) + @test getbounds(timg).rs == (1:32, 1:48) + @test !any(isnan.(timg |> itemdata)) img = rand(RGB{N0f8}, 196, 196) timg = apply(tfm, Image(img)) |> itemdata - @test timg |> size == (32, 32) - @test !any(isnan.(timg)) - end + timg = apply(tfm, Image(img)) + @test getbounds(timg).rs == (1:32, 1:32) + @test !any(isnan.(timg |> itemdata)) end @testset ExtendedTestSet "Rotate" begin From 62710cb049229c2cd5a0daab0865106a8b57ac80 Mon Sep 17 00:00:00 2001 From: Heiner Spiess Date: Tue, 4 Nov 2025 18:51:14 +0100 Subject: [PATCH 5/5] syntax error slipped through --- test/projective/affine.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/projective/affine.jl b/test/projective/affine.jl index cf1647f..519d5d2 100644 --- a/test/projective/affine.jl +++ b/test/projective/affine.jl @@ -120,6 +120,7 @@ include("../imports.jl") timg = apply(tfm, Image(img)) @test getbounds(timg).rs == (1:32, 1:32) @test !any(isnan.(timg |> itemdata)) + end end @testset ExtendedTestSet "Rotate" begin