Skip to content

Commit 19d09a8

Browse files
authored
Better resample (#425)
* update resample * add size kw to resample and test * bugfix resample to geom
1 parent 4fc88c8 commit 19d09a8

File tree

5 files changed

+181
-57
lines changed

5 files changed

+181
-57
lines changed

src/array.jl

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ function crs(obj)
6565
nothing
6666
end
6767
end
68+
crs(::Nothing) = nothing
6869
crs(dim::Dimension) = crs(lookup(dim))
6970

7071
"""

src/methods/mask.jl

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ const TO_KEYWORD = """
66
"""
77
const SIZE_KEYWORD = """
88
- `size`: the size of the output array, as a `Tuple{Int,Int}` or single `Int` for a square.
9-
Only required when `to` is not used or is an `Extents.Extent`, otherwise `size`.
9+
Only required when `to` is not used or is an `Extents.Extent`, and `res` is not used.
1010
"""
1111
const RES_KEYWORD = """
1212
- `res`: the resolution of the dimensions, a `Real` or `Tuple{<:Real,<:Real}`.
13+
Only required when `to` is not used or is an `Extents.Extent`, and `size` is not used.
1314
"""
1415
const CRS_KEYWORD = """
1516
- `crs`: a `crs` which will be attached to the resulting raster when `to` not passed

src/methods/resample.jl

+104-50
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
"""
2-
resample(x, resolution::Number; crs, method)
3-
resample(x; to, method)
4-
resample(xs...; to=first(xs), method)
2+
resample(x; to, size, res, method)
3+
resample(xs...; to=first(xs), size, res, method)
54
65
`resample` uses `ArchGDAL.gdalwarp` to resample a [`Raster`](@ref) or
76
[`RasterStack`](@ref) to a new `resolution` and optionally new `crs`,
87
or to snap to the bounds, resolution and crs of the object `to`.
98
109
# Arguments
1110
12-
- `x`: the object to resample.
13-
- `resolution`: a `Number` specifying the resolution for the output.
14-
If the keyword argument `crs` (described below) is specified, `resolution` must be in units of the `crs`.
11+
- `x`: the object/s to resample.
1512
1613
# Keywords
1714
18-
- `to`: an `AbstractRaster` whose resolution, crs and bounds will be snapped to.
19-
For best results it should roughly cover the same extent, or be a subset of `A`.
20-
- `crs`: A `GeoFormatTypes.GeoFormat` such as `EPSG(x)` or `WellKnownText(string)` specifying an
21-
output crs (`A` will be reprojected to `crs` in addition to being resampled). Defaults to `crs(A)`
15+
- `to`: a `Raster`, `RasterStack`, `Tuple` of `Dimension` or `Extents.Extent`.
16+
If no `to` object is provided the extent will be calculated from `x`,
17+
$RES_KEYWORD
18+
$SIZE_KEYWORD
19+
- `crs`: A `GeoFormatTypes.GeoFormat` coordinate reference system for the output raster,
20+
such as `EPSG(x)` or `WellKnownText(string)`. Defaults to `crs(A)`.
2221
- `method`: A `Symbol` or `String` specifying the method to use for resampling.
2322
From the docs for [`gdalwarp`](https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r):
2423
* `:near`: nearest neighbour resampling (default, fastest algorithm, worst interpolation quality).
@@ -38,12 +37,9 @@ or to snap to the bounds, resolution and crs of the object `to`.
3837
3938
Where NODATA values are set to `missingval`.
4039
41-
Notes:
42-
- `missingval` of `missing` does not work with GDAL. Use `replace_missing(A, newmissingval)` to
43-
assign a missing value before using `resample` if the current value is `missing`. This will be
44-
automated in future versions.
45-
- GDAL may cause some unexpected changes in the data, such as returning a reversed dimension or
46-
changing the `crs` datatype from `EPSG` to `WellKnownText`.
40+
Note:
41+
- GDAL may cause some unexpected changes in the data, such as returning a reversed Y dimension or
42+
changing the `crs` type from `EPSG` to `WellKnownText` (it will represent the same CRS).
4743
4844
# Example
4945
@@ -73,49 +69,107 @@ savefig(b, "build/resample_example_after.png"); nothing
7369
$EXPERIMENTAL
7470
"""
7571
function resample end
72+
resample(x, res; kw...) = resample(x; res, kw...)
7673
resample(xs::RasterStackOrArray...; kw...) = resample(xs; kw...)
7774
function resample(ser::AbstractRasterSeries, args...; kw...)
7875
map(x -> resample(x, args...; kw...), ser)
7976
end
8077
function resample(xs::Union{Tuple,NamedTuple}; to=first(xs), kw...)
8178
map(x -> resample(x; to, kw...), xs)
8279
end
83-
function resample(A::RasterStackOrArray, resolution::Number; kw...)
84-
dimres = map(d -> DD.basetypeof(d)(resolution), dims(A, (XDim, YDim)))
85-
resample(A, dimres; kw...)
86-
end
87-
function resample(A::RasterStackOrArray, pairs::Pair...; kw...)
88-
resample(A, map((D, x) -> D(x), pairs); kw...)
89-
end
90-
function resample(A::RasterStackOrArray, resdims::DimTuple;
91-
crs::GeoFormat=crs(A), method=:near, kw...
80+
function resample(x::RasterStackOrArray;
81+
# We need to combine the `size` and `res` keywords with
82+
# the extent in extent2dims, even if we already have dims.
83+
to=nothing, res=nothing, crs=nothing, size=nothing, method=:near, kw...
9284
)
93-
wkt = convert(String, convert(WellKnownText, crs))
94-
res = map(val, dims(resdims, (YDim, XDim)))
95-
flags = Dict(
96-
:t_srs => wkt,
97-
:tr => res,
98-
:r => method,
99-
)
100-
return warp(A, flags; kw...)
101-
end
102-
function resample(A::RasterStackOrArray; to, method=:near, kw...)
103-
all(hasdim(to, (XDim, YDim))) || throw(ArgumentError("`to` mush have both XDim and YDim dimensions to resize with GDAL"))
104-
if sampling(to, XDim) isa Points
105-
to = set(to, dims(to, XDim) => Intervals(Start()))
85+
(isnothing(size) || isnothing(res)) || _size_and_res_error()
86+
87+
# Flags to send to `warp`, then to GDAL
88+
flags = Dict{Symbol,Any}()
89+
90+
# Method
91+
flags[:r] = method
92+
93+
# Extent
94+
if to isa Extents.Extent || isnothing(dims(to))
95+
to = isnothing(to) || to isa Extents.Extent ? to : GeoInterface.extent(to)
96+
if !isnothing(to)
97+
# Get the extent of geometries
98+
(xmin, xmax), (ymin, ymax) = to[(:X, :Y)]
99+
flags[:te] = [xmin, ymin, xmax, ymax]
100+
end
101+
else
102+
all(hasdim(to, (XDim, YDim))) || throw(ArgumentError("`to` mush have both XDim and YDim dimensions to resize with GDAL"))
103+
if sampling(to, XDim) isa Points
104+
to = set(to, dims(to, XDim) => Intervals(Start()))
105+
end
106+
if sampling(to, YDim) isa Points
107+
to = set(to, dims(to, YDim) => Intervals(Start()))
108+
end
109+
110+
# Set res from `to` if it was not already set
111+
if isnothing(res) && isnothing(size)
112+
xres, yres = map(abs step, span(to, (XDim, YDim)))
113+
flags[:tr] = [yres, xres]
114+
end
115+
(xmin, xmax), (ymin, ymax) = bounds(to, (XDim, YDim))
116+
flags[:te] = [xmin, ymin, xmax, ymax]
106117
end
107-
if sampling(to, YDim) isa Points
108-
to = set(to, dims(to, YDim) => Intervals(Start()))
118+
119+
# CRS
120+
crs = if isnothing(crs)
121+
if to isa Extents.Extent
122+
nothing
123+
else
124+
# get crs from `to` or `x` if none was passed in
125+
isnothing(Rasters.crs(to)) ? Rasters.crs(x) : Rasters.crs(to)
126+
end
127+
else
128+
crs
129+
end
130+
if !isnothing(crs)
131+
wkt = convert(String, convert(WellKnownText, crs))
132+
flags[:t_srs] = wkt
133+
if isnothing(Rasters.crs(x))
134+
@warn "You have set a crs to resample to, but the object does not have crs so GDAL will assume it is already in the target crs. Use `newraster = setcrs(raster, somecrs)` to fix this."
135+
end
109136
end
110137

111-
wkt = convert(String, convert(WellKnownText, crs(to)))
112-
xres, yres = map(abs step, span(to, (XDim, YDim)))
113-
(xmin, xmax), (ymin, ymax) = bounds(to, (XDim, YDim))
114-
flags = Dict(
115-
:t_srs => wkt,
116-
:tr => [yres, xres],
117-
:te => [xmin, ymin, xmax, ymax],
118-
:r => method,
119-
)
120-
return warp(A, flags; kw...)
138+
# Resolution
139+
if !isnothing(res)
140+
xres, yres = if res isa Real
141+
res, res
142+
elseif res isa Tuple{<:Dimension{<:Real},<:Dimension{<:Real}}
143+
map(val, dims(res, (YDim, XDim)))
144+
elseif res isa Tuple{<:Real,<:Real}
145+
reverse(res)
146+
else
147+
throw(ArgumentError("`res` must be a `Real`, or a 2 `Tuple` of `Real` or `Dimension`s wrapping `Real`. Got $res"))
148+
end
149+
flags[:tr] = [yres, xres]
150+
end
151+
152+
# Size
153+
if !isnothing(size)
154+
xsize, ysize = if size isa Int
155+
size, size
156+
elseif size isa Tuple{<:Dimension{Int},<:Dimension{Int}}
157+
map(val, dims(size, (YDim, XDim)))
158+
elseif size isa Tuple{Int,Int}
159+
reverse(size)
160+
else
161+
throw(ArgumentError("`size` must be a `Int`, or a 2 `Tuple` of `Int` or `Dimension`s wrapping `Int`. Got $size"))
162+
end
163+
flags[:ts] = [ysize, xsize]
164+
end
165+
166+
# resample with `warp`
167+
resampled = warp(x, flags; kw...)
168+
169+
# Return crs to the original type, from GDAL it will always be WellKnownText
170+
if isnothing(crs)
171+
return setcrs(resampled, Rasters.crs(x))
172+
else
173+
return setcrs(resampled, crs)
174+
end
121175
end

src/utils.jl

+3-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ function _extent2dims(to::Extents.Extent, size::Nothing, res::Nothing, crs)
137137
isnothing(res) && throw(ArgumentError("Pass either `size` or `res` keywords or a `Tuple` of `Dimension`s for `to`."))
138138
end
139139
function _extent2dims(to::Extents.Extent, size, res, crs)
140-
isnothing(res) || throw(ArgumentError("Both `size` and `res` keywords are passed, but only one can be used"))
140+
isnothing(res) || _size_and_res_error()
141141
end
142142
function _extent2dims(to::Extents.Extent{K}, size::Nothing, res::Real, crs) where K
143143
tuple_res = ntuple(_ -> res, length(K))
@@ -199,6 +199,8 @@ _progress(args...; kw...) = ProgressMeter.Progress(args...; color=:blue, barlen=
199199
# Function barrier for splatted vector broadcast
200200
@noinline _do_broadcast!(f, x, args...) = broadcast!(f, x, args...)
201201

202+
_size_and_res_error() = throw(ArgumentError("Both `size` and `res` keywords are passed, but only one can be used"))
203+
202204
_type_missingval(::Type{T}) where T = typemin(T)
203205
_type_missingval(::Type{T}) where T<:Unsigned = typemax(T)
204206

test/methods.jl

+71-5
Original file line numberDiff line numberDiff line change
@@ -702,25 +702,25 @@ end
702702

703703
output_res = 0.0027
704704
output_crs = EPSG(4326)
705-
resample_method = "near"
705+
method = "near"
706706

707707
## Resample cea.tif manually with ArchGDAL
708708
wkt = convert(String, convert(WellKnownText, output_crs))
709709
AG_output = ArchGDAL.read(raster_path) do dataset
710710
ArchGDAL.gdalwarp([dataset], ["-t_srs", "$(wkt)",
711711
"-tr", "$(output_res)", "$(output_res)",
712-
"-r", "$(resample_method)"]) do warped
712+
"-r", "$(method)"]) do warped
713713
ArchGDAL.read(ArchGDAL.getband(warped, 1))
714714
end
715715
end
716716

717717
# Resample cea.tif using resample
718718
cea = Raster(raster_path; missingval=0x00)
719-
raster_output = resample(cea, output_res; crs=output_crs, method=resample_method)
720-
disk_output = resample(cea, output_res; crs=output_crs, method=resample_method, filename="resample.tif")
719+
raster_output = resample(cea; res=output_res, crs=output_crs, method)
720+
disk_output = resample(cea; res=output_res, crs=output_crs, method, filename="resample.tif")
721721

722722
cea_permuted = permutedims(Raster(raster_path), (Y, X, Band))
723-
permuted_output = resample(cea_permuted, output_res; crs=output_crs, method=resample_method)
723+
permuted_output = resample(cea_permuted, output_res; crs=output_crs, method)
724724

725725
# Compare ArchGDAL, resample and permuted resample
726726
@test AG_output ==
@@ -744,4 +744,70 @@ end
744744
@test isapprox(index(snaptarget, Y), index(disk_snapped, Y))
745745
@test isapprox(index(snaptarget, X), index(disk_snapped, X))
746746
end
747+
748+
@testset "`method` only does nothing" begin
749+
resampled = resample(cea; method)
750+
@test crs(cea) == crs(resampled)
751+
@test cea == resampled
752+
# There is some floating point error here after Rasters -> GDAL -> Rasterss...
753+
# Should we correct it by detecting almost identical extent and using the original?
754+
@test_broken extent(cea) = extent(resampled)
755+
end
756+
757+
@testset "only `res` kw changes the array size predictably" begin
758+
res = step(span(cea, X)) / 2
759+
resampled = resample(cea; res)
760+
@test crs(cea) == crs(resampled)
761+
@test size(dims(resampled, (X, Y))) == size(dims(cea, (X, Y))) .* 2
762+
# GDAL fp error see above
763+
@test_broken extent(cea) = extent(resampled)
764+
resampled = resample(cea; res=(res, 2res))
765+
@test size(dims(resampled, (X, Y))) == (size(cea, X) .* 2, size(cea, Y))
766+
resampled = resample(cea; res=(X(2res), Y(res)))
767+
@test size(dims(resampled, (X, Y))) == (size(cea, X), size(cea, Y) * 2)
768+
end
769+
770+
@testset "only `size` kw sets the size" begin
771+
res = step(span(cea, X)) / 2
772+
resampled = resample(cea; size=(100, 200))
773+
@test crs(cea) == crs(resampled)
774+
@test size(dims(resampled, (X, Y))) == size(resampled[:, :, 1]) == (100, 200)
775+
resampled = resample(cea; size=(X(99), Y(111)))
776+
@test crs(cea) == crs(resampled)
777+
@test size(dims(resampled, (X, Y))) == size(resampled[:, :, 1]) == (99, 111)
778+
resampled = resample(cea; size=100)
779+
@test size(dims(resampled, (X, Y))) == size(resampled[:, :, 1]) == (100, 100)
780+
end
781+
782+
@testset "Extent `to` can resize arbitrarily" begin
783+
to = Extent(X=(-10000.0, 2000.0), Y=(4.2e6, 4.3e6))
784+
resampled = resample(cea; to)
785+
@test all(map((bs...,) -> all(map(, bs...)), extent(resampled), to))
786+
@test crs(cea) == crs(resampled)
787+
# Most of the area is extended, and filled with missingval
788+
@test sum(x -> x === missingval(resampled), resampled) > length(resampled) / 2
789+
end
790+
791+
@testset "Geometries work as `to`" begin
792+
geom = GeoInterface.Polygon([[(0.0, 4e6), (-1e5, 4.4e6), (-1e5, 4.2e6)]])
793+
resampled = resample(cea; to=geom)
794+
@test map(extent(resampled[Band(1)]), GeoInterface.extent(geom)) do bs1, bs2
795+
map(, bs2, bs2) |> all
796+
end |> all
797+
@test crs(cea) == crs(resampled)
798+
# Most of the area is extended, and filled with missingval
799+
@test sum(x -> x === missingval(resampled), resampled) > length(resampled) / 2
800+
end
801+
802+
@testset "only `crs` kw changes the array size" begin
803+
resampled = resample(cea; crs=EPSG(3857), method)
804+
@test size(dims(resampled, (X, Y))) !== size(dims(cea, (X, Y)))
805+
@test crs(resampled) == EPSG(3857)
806+
end
807+
808+
@testset "no existing crs warns" begin
809+
nocrs = setcrs(cea, nothing)
810+
@test crs(nocrs) == nothing
811+
@test_warn "does not have crs" resample(nocrs; crs=output_crs, method)
812+
end
747813
end

0 commit comments

Comments
 (0)