Skip to content

Conversation

@h-spiess
Copy link

Scale projections had seperate implementations where the black border was only fixed for ScaleFixed. Unified implementations and fixed black borders for all projections.

PR Checklist

  • Tests are added
  • Documentation, if applicable

@h-spiess
Copy link
Author

This fixes #102

@h-spiess h-spiess changed the title Unified Scale projections and fixed black bordern Unified Scale projections and fixed black border Oct 21, 2025
@h-spiess
Copy link
Author

h-spiess commented Oct 21, 2025

Example from tests on master vs. this commit:

current master:
plot_scale_ratio_before

this commit:
plot_scale_ratio_after

Copy link
Collaborator

@paulnovo paulnovo left a comment

Choose a reason for hiding this comment

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

Thank you, this is great. Consolidating the scale projections makes a lot of sense. I added just one comment/question.

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)
Copy link
Collaborator

@paulnovo paulnovo Oct 23, 2025

Choose a reason for hiding this comment

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

Transforming a 50x50 image with ScaleRatio((1/2, 1/2)), I would expect this to be (1:25, 1:25). With your other fixes, and using a 0.5 offset instead of 1.0 in projectionbounds, this can be adjusted to (1:25, 1:25):

--- a/src/projective/affine.jl
+++ b/src/projective/affine.jl
@@ -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

Would this reintroduce the black border you are trying to fix? (it does look like all the other unit tests pass with this change).

Copy link
Author

@h-spiess h-spiess Oct 24, 2025

Choose a reason for hiding this comment

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

Thanks for checking out the PR. I'm fine with this change. I've just sticked to an offset of 1.0 as it has been used before and is still used for the translation in getprojection. I guess they don't have to be consistent?

See:

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
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))

vs.

upperleft = SVector{N, Float32}(minimum.(bounds.rs)) .- 1
P = scaleprojection(ratios)
if any(upperleft .!= 0)
P = P Translation(-upperleft)
end
return P
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))

Copy link
Author

@h-spiess h-spiess Oct 24, 2025

Choose a reason for hiding this comment

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

I've added it but it somehow screws up two consecutive scalings. I will try to resolve the issue.

Copy link
Collaborator

@paulnovo paulnovo left a comment

Choose a reason for hiding this comment

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

Thanks for digging into the bounds ranges, this is really great. I just left a couple of small comments and tweaks. But this is looking ready to go.

Comment on lines +115 to +117
timg = apply(tfm, Image(img)) |> itemdata
@test timg |> size == (32, 48)
@test !any(isnan.(timg))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Check bounds ranges here too:

Suggested change
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))

Comment on lines +120 to +122
timg = apply(tfm, Image(img)) |> itemdata
@test timg |> size == (32, 32)
@test !any(isnan.(timg))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here

Suggested change
timg = apply(tfm, Image(img)) |> itemdata
@test timg |> size == (32, 32)
@test !any(isnan.(timg))
timg = apply(tfm, Image(img))
@test getbounds(timg).rs == (1:32, 1:32)
@test !any(isnan.(timg |> itemdata))


@testset ExtendedTestSet "ScaleRatioTwice" begin
tfm = ScaleRatio((4/5, 4/5)) |> ScaleRatio((1/2, 1/2))
@show getbounds(image).rs
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please remove the @show

Comment on lines +57 to +64
@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
Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh wow, ok. Nice catch here. We might eventually want to think about just writing an apply for ComposedProjectiveTransform so we don't calculate P twice (here and in getprojection), but your approach is probably the safest way forward.

timage = apply(tfm, image)
tkeypoints = apply(tfm, keypoints)
@test !any(isnan.(timage |> itemdata))
@test length.(getbounds(timage).rs) == (25, 25)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would you mind adding checks to the full range range in these tests, not just the size. Down the road, I can image the ranges becoming incorrect again without a test

Suggested change
@test length.(getbounds(timage).rs) == (25, 25)
@test getbounds(timage).rs == (1:25, 1:25)

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