Skip to content

Commit e846044

Browse files
authored
Breaking: Line and Polygon extract optimisation (#824)
* faster line extract * performance * extract in a separate file * flatten * bugfixes * some more comments * more perf tweaks * bugfix * fix extract ambiguities * more ambiguity * Bugfix geomtrait * arg fixes * add id column * tests and bugfixes * bugfix * set id default to _False() * fix tests
1 parent b70119b commit e846044

File tree

16 files changed

+1086
-497
lines changed

16 files changed

+1086
-497
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ ConstructionBase = "1"
6060
CoordinateTransformations = "0.6.2"
6161
DataFrames = "1"
6262
DimensionalData = "0.29.4"
63-
DiskArrays = "0.3, 0.4"
63+
DiskArrays = "0.4"
6464
Extents = "0.1"
6565
FillArrays = "0.12, 0.13, 1"
6666
Flatten = "0.4"

ext/RastersStatsBaseExt/sample.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ function _getindex(::Type{T}, x::AbstractRasterStack{<:Any, NT}, dims, idx) wher
5252
RA._maybe_add_fields(
5353
T,
5454
NT(x[RA.commondims(idx, x)]),
55+
nothing,
5556
DimPoints(dims)[RA.commondims(idx, dims)],
5657
val(idx)
5758
)

src/methods/burning/allocs.jl

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,36 @@ function Allocs(buffer)
1111
return Allocs(buffer, edges, scratch, crossings)
1212
end
1313

14-
function _burning_allocs(x; nthreads=_nthreads(), threaded=true, kw...)
15-
dims = commondims(x, DEFAULT_POINT_ORDER)
14+
function _burning_allocs(x;
15+
nthreads=_nthreads(),
16+
threaded=true,
17+
burncheck_metadata=Metadata(),
18+
kw...
19+
)
1620
if threaded
17-
[Allocs(_init_bools(dims; metadata=Metadata())) for _ in 1:nthreads]
21+
if isnothing(x)
22+
[Allocs(nothing) for _ in 1:nthreads]
23+
else
24+
dims = commondims(x, DEFAULT_POINT_ORDER)
25+
[Allocs(_init_bools(dims; metadata=deepcopy(burncheck_metadata))) for _ in 1:nthreads]
26+
end
1827
else
19-
Allocs(_init_bools(dims; metadata=Metadata()))
28+
if isnothing(x)
29+
Allocs()
30+
else
31+
dims = commondims(x, DEFAULT_POINT_ORDER)
32+
Allocs(_init_bools(dims; metadata=burncheck_metadata))
33+
end
2034
end
2135
end
2236

2337
_get_alloc(allocs::Vector{<:Allocs}) = _get_alloc(allocs[Threads.threadid()])
2438
_get_alloc(allocs::Allocs) = allocs
2539

26-
2740
# TODO include these in Allocs
2841
_alloc_burnchecks(n::Int) = fill(false, n)
2942
_alloc_burnchecks(x::AbstractArray) = _alloc_burnchecks(length(x))
43+
3044
function _set_burnchecks(burnchecks, metadata::Metadata{<:Any,<:Dict}, verbose)
3145
metadata["missed_geometries"] = .!burnchecks
3246
verbose && _burncheck_info(burnchecks)

src/methods/burning/array_init.jl

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11

2-
# Like `create` but without disk writes, mostly for Bool/Union{Missing,Boo},
2+
# Like `create` but without disk writes, mostly for Bool or Union{Missing,Bool},
33
# and uses `similar` where possible
44
# TODO merge this with `create` somehow
55
_init_bools(to; kw...) = _init_bools(to, BitArray; kw...)
66
_init_bools(to, T::Type; kw...) = _init_bools(to, T, nothing; kw...)
7-
_init_bools(to::AbstractRasterSeries, T::Type, data; kw...) = _init_bools(first(to), T, data; kw...)
8-
_init_bools(to::AbstractRasterStack, T::Type, data; kw...) = _init_bools(first(to), T, data; kw...)
9-
_init_bools(to::AbstractRaster, T::Type, data; kw...) = _init_bools(to, dims(to), T, data; kw...)
10-
_init_bools(to::Extents.Extent, T::Type, data; kw...) = _init_bools(to, _extent2dims(to; kw...), T, data; kw...)
11-
_init_bools(to::DimTuple, T::Type, data; kw...) = _init_bools(to, to, T, data; kw...)
7+
_init_bools(to::AbstractRasterSeries, T::Type, data; kw...) =
8+
_init_bools(dims(first(to)), T, data; kw...)
9+
_init_bools(to::AbstractRasterStack, T::Type, data; kw...) =
10+
_init_bools(dims(to), dims(to), T, data; kw...)
11+
_init_bools(to::AbstractRaster, T::Type, data; kw...) =
12+
_init_bools(to, dims(to), T, data; kw...)
13+
_init_bools(to::Extents.Extent, T::Type, data; kw...) =
14+
_init_bools(to, _extent2dims(to; kw...), T, data; kw...)
15+
_init_bools(to::DimTuple, T::Type, data; kw...) =
16+
_init_bools(to, to, T, data; kw...)
1217
function _init_bools(to::Nothing, T::Type, data; geometrycolumn=nothing,kw...)
1318
# Get the extent of the geometries
1419
ext = _extent(data; geometrycolumn, kw...)
@@ -38,7 +43,7 @@ function _alloc_bools(to, dims::DimTuple, ::Type{BitArray}; missingval::Bool=fal
3843
end
3944
function _alloc_bools(to, dims::DimTuple, ::Type{<:Array{T}}; missingval=false, metadata=NoMetadata(), kw...) where T
4045
# Use an `Array`
41-
data = fill!(Raster{T}(undef, dims), missingval)
46+
data = fill!(Raster{T}(undef, dims), missingval)
4247
return rebuild(data; missingval, metadata)
4348
end
4449

@@ -53,7 +58,9 @@ function _prepare_for_burning(B, locus=Center())
5358
end
5459

5560
# Convert to Array if its not one already
56-
_lookup_as_array(d::Dimension) = parent(lookup(d)) isa Array ? d : modify(Array, d)
61+
_lookup_as_array(x) = setdims(x, _lookup_as_array(dims(x)))
62+
_lookup_as_array(dims::Tuple) = map(_lookup_as_array, dims)
63+
_lookup_as_array(d::Dimension) = parent(lookup(d)) isa Array ? d : modify(Array, d)
5764

5865
function _forward_ordered(B)
5966
reduce(dims(B); init=B) do A, d

src/methods/burning/geometry.jl

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ function _burn_geometry!(B::AbstractRaster, trait::Nothing, data;
2222
lock=Threads.SpinLock(),
2323
verbose=true,
2424
progress=true,
25-
threaded=true,
25+
threaded=false,
2626
fill=true,
27-
allocs=_burning_allocs(B; threaded),
2827
geometrycolumn=nothing,
28+
burncheck_metadata=Metadata(),
29+
allocs=_burning_allocs(B; threaded, burncheck_metadata),
2930
kw...
3031
)::Bool
3132
geoms = _get_geometries(data, geometrycolumn)
@@ -36,8 +37,8 @@ function _burn_geometry!(B::AbstractRaster, trait::Nothing, data;
3637
geom = _getgeom(geoms, i)
3738
ismissing(geom) && return nothing
3839
a = _get_alloc(allocs)
39-
B1 = a.buffer
40-
burnchecks[i] = _burn_geometry!(B1, geom; fill, allocs=a, lock, kw...)
40+
buffer = a.buffer
41+
burnchecks[i] = _burn_geometry!(buffer, geom; fill, allocs=a, lock, kw...)
4142
return nothing
4243
end
4344
if fill
@@ -99,14 +100,17 @@ function _burn_geometry!(B::AbstractRaster, ::GI.AbstractGeometryTrait, geom;
99100
return false
100101
end
101102
# Take a view of the geometry extent
102-
B1 = view(B, Touches(geomextent))
103-
buf1 = _init_bools(B1; missingval=false)
103+
V = view(B, Touches(geomextent))
104+
buffer = _init_bools(V; missingval=false)
104105
# Burn the polygon into the buffer
105-
allocs = isnothing(allocs) ? Allocs(B) : allocs
106-
hasburned = _burn_polygon!(buf1, geom; shape, geomextent, allocs, boundary, kw...)
107-
@inbounds for i in eachindex(B1)
108-
if buf1[i]
109-
B1[i] = fill
106+
# We allocate a new bitarray for the view for performance
107+
# and always fill with `true`.
108+
allocs = isnothing(allocs) ? Allocs(nothing) : allocs
109+
hasburned = _burn_polygon!(buffer, geom; shape, geomextent, allocs, boundary, kw...)
110+
# We then transfer burned `true` values to B via the V view
111+
@inbounds for i in eachindex(V)
112+
if buffer[i]
113+
V[i] = fill
110114
end
111115
end
112116
else

src/methods/burning/line.jl

Lines changed: 63 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,53 @@
11
# _burn_lines!
22
# Fill a raster with `fill` where pixels touch lines in a geom
3-
# Separated for a type stability function barrier
4-
function _burn_lines!(B::AbstractRaster, geom; fill=true, kw...)
5-
_check_intervals(B)
3+
# Usually `fill` is `true` of `false`
4+
function _burn_lines!(
5+
B::AbstractRaster, geom; fill=true, verbose=false, kw...
6+
)
7+
_check_intervals(B, verbose)
68
B1 = _prepare_for_burning(B)
7-
return _burn_lines!(B1, geom, fill)
9+
10+
xdim, ydim = dims(B, DEFAULT_POINT_ORDER)
11+
regular = map((xdim, ydim)) do d
12+
# @assert (parent(lookup(d)) isa Array)
13+
lookup(d) isa AbstractSampled && span(d) isa Regular
14+
end
15+
msg = """
16+
Can only fill lines where dimensions have `Regular` lookups.
17+
Consider using `boundary=:center`, reprojecting the crs,
18+
or make an issue in Rasters.jl on github if you need this to work.
19+
"""
20+
all(regular) || throw(ArgumentError(msg))
21+
22+
# Set indices of B as `fill` when a cell is found to burn.
23+
_burn_lines!(identity, dims(B1), geom) do D
24+
@inbounds B1[D] = fill
25+
end
826
end
927

10-
_burn_lines!(B, geom, fill) =
11-
_burn_lines!(B, GI.geomtrait(geom), geom, fill)
12-
function _burn_lines!(B::AbstractArray, ::Union{GI.MultiLineStringTrait}, geom, fill)
28+
_burn_lines!(f::F, c::C, dims::Tuple, geom) where {F<:Function,C<:Function} =
29+
_burn_lines!(f, c, dims, GI.geomtrait(geom), geom)
30+
function _burn_lines!(
31+
f::F, c::C, dims::Tuple, ::Union{GI.MultiLineStringTrait}, geom
32+
) where {F<:Function,C<:Function}
1333
n_on_line = 0
1434
for linestring in GI.getlinestring(geom)
15-
n_on_line += _burn_lines!(B, linestring, fill)
35+
n_on_line += _burn_lines!(f, c, dims, linestring)
1636
end
1737
return n_on_line
1838
end
1939
function _burn_lines!(
20-
B::AbstractArray, ::Union{GI.MultiPolygonTrait,GI.PolygonTrait}, geom, fill
21-
)
40+
f::F, c::C, dims::Tuple, ::Union{GI.MultiPolygonTrait,GI.PolygonTrait}, geom
41+
) where {F<:Function,C<:Function}
2242
n_on_line = 0
2343
for ring in GI.getring(geom)
24-
n_on_line += _burn_lines!(B, ring, fill)
44+
n_on_line += _burn_lines!(f, c, dims, ring)
2545
end
2646
return n_on_line
2747
end
2848
function _burn_lines!(
29-
B::AbstractArray, ::GI.AbstractCurveTrait, linestring, fill
30-
)
49+
f::F, c::C, dims::Tuple, ::GI.AbstractCurveTrait, linestring
50+
) where {F<:Function,C<:Function}
3151
isfirst = true
3252
local firstpoint, laststop
3353
n_on_line = 0
@@ -46,46 +66,42 @@ function _burn_lines!(
4666
stop=(x=GI.x(point), y=GI.y(point)),
4767
)
4868
laststop = line.stop
49-
n_on_line += _burn_line!(B, line, fill)
69+
n_on_line += _burn_line!(f, c, dims, line)
5070
end
5171
return n_on_line
5272
end
5373
function _burn_lines!(
54-
B::AbstractArray, t::GI.LineTrait, line, fill
55-
)
74+
f::F, c::C, dims::Tuple, t::GI.LineTrait, line
75+
) where {F<:Function,C<:Function}
5676
p1, p2 = GI.getpoint(t, line)
5777
line1 = (
5878
start=(x=GI.x(p1), y=GI.y(p1)),
5979
stop=(x=GI.x(p2), y=GI.y(p2)),
6080
)
61-
return _burn_line!(B, line1, fill)
81+
return _burn_line!(f, c, dims, line1)
6282
end
6383

6484
# _burn_line!
6585
#
6686
# Line-burning algorithm
6787
# Burns a single line into a raster with value where pixels touch a line
6888
#
89+
# Function `f` does the actual work when passed a Dimension Tuple of a pixel to burn,
90+
# and `c` is an initialisation callback that is passed the maximyum
91+
# number of times `f` will be called. It may be called less than that.
92+
#
6993
# TODO: generalise to Irregular spans?
70-
function _burn_line!(A::AbstractRaster, line, fill)
71-
72-
xdim, ydim = dims(A, DEFAULT_POINT_ORDER)
73-
regular = map((xdim, ydim)) do d
74-
@assert (parent(lookup(d)) isa Array)
75-
lookup(d) isa AbstractSampled && span(d) isa Regular
76-
end
77-
msg = """
78-
Can only fill lines where dimensions have `Regular` lookups.
79-
Consider using `boundary=:center`, reprojecting the crs,
80-
or make an issue in Rasters.jl on github if you need this to work.
81-
"""
82-
all(regular) || throw(ArgumentError(msg))
94+
function _burn_line!(f::Function, c::Function, dims::Tuple, line::NamedTuple)
95+
xdim, ydim = DD.dims(dims, DEFAULT_POINT_ORDER)
96+
di = DimIndices(dims)
8397

98+
@assert xdim isa XDim
99+
@assert ydim isa YDim
84100
@assert order(xdim) == order(ydim) == Lookups.ForwardOrdered()
85101
@assert locus(xdim) == locus(ydim) == Lookups.Center()
86102

87-
raster_x_step = abs(step(span(A, X)))
88-
raster_y_step = abs(step(span(A, Y)))
103+
raster_x_step = abs(step(span(xdim)))
104+
raster_y_step = abs(step(span(ydim)))
89105
raster_x_offset = @inbounds xdim[1] - raster_x_step / 2 # Shift from center to start of pixel
90106
raster_y_offset = @inbounds ydim[1] - raster_y_step / 2
91107

@@ -117,15 +133,18 @@ function _burn_line!(A::AbstractRaster, line, fill)
117133
# Int starting points for the line. +1 converts to julia indexing
118134
j, i = trunc(Int, relstart.x) + 1, trunc(Int, relstart.y) + 1 # Int
119135

120-
# For arbitrary dimension indexing
121-
dimconstructors = map(DD.basetypeof, (xdim, ydim))
136+
n_on_line = 0
137+
138+
# Pass of number of runs of `f` to callback `c`
139+
# This can help with e.g. allocating a vector
140+
c(manhattan_distance + 1)
122141

123142
if manhattan_distance == 0
124-
D = map((d, o) -> d(o), dimconstructors, (j, i))
125-
if checkbounds(Bool, A, D...)
126-
@inbounds A[D...] = fill
143+
D = map(rebuild, dims, (j, i))
144+
if checkbounds(Bool, di, D...)
145+
f(D)
146+
n_on_line += 1
127147
end
128-
n_on_line = 1
129148
return n_on_line
130149
end
131150

@@ -153,18 +172,17 @@ function _burn_line!(A::AbstractRaster, line, fill)
153172
n_on_line = 0
154173
countx = county = 0
155174

156-
157175
# Int steps to move allong the line
158176
step_j = signbit(diff_x) * -2 + 1
159177
step_i = signbit(diff_y) * -2 + 1
160178

161179
# Travel one grid cell at a time. Start at zero for the current cell
162180
for _ in 0:manhattan_distance
163-
D = map((d, o) -> d(o), dimconstructors, (j, i))
164-
if checkbounds(Bool, A, D...)
165-
@inbounds A[D...] = fill
181+
D = map(rebuild, dims, (j, i))
182+
if checkbounds(Bool, di, D...)
183+
f(D)
184+
n_on_line += 1
166185
end
167-
168186
# Only move in either X or Y coordinates, not both.
169187
if abs(max_x) < abs(max_y)
170188
max_x += delta_x
@@ -176,12 +194,6 @@ function _burn_line!(A::AbstractRaster, line, fill)
176194
county +=1
177195
end
178196
end
179-
return n_on_line
180-
end
181-
function _burn_line!(A::AbstractRaster, line, fill, order::Tuple{Vararg{Dimension}})
182-
msg = """"
183-
Converting a `:line` geometry to raster is currently only implemented for 2d lines.
184-
Make a Rasters.jl github issue if you need this for more dimensions.
185-
"""
186-
throw(ArgumentError(msg))
197+
198+
return n_on_line
187199
end

src/methods/burning/polygon.jl

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ function _burn_polygon!(B::AbstractDimArray, trait, geom;
2424
# Lines
2525
n_on_line = 0
2626
if boundary !== :center
27-
_check_intervals(B, boundary)
27+
_check_intervals(B, boundary, verbose)
2828
if boundary === :touches
29-
if _check_intervals(B, boundary)
29+
if _check_intervals(B, boundary, verbose)
3030
# Add line pixels
3131
n_on_line = _burn_lines!(B, geom; fill)::Int
3232
end
3333
elseif boundary === :inside
34-
if _check_intervals(B, boundary)
34+
if _check_intervals(B, boundary, verbose)
3535
# Remove line pixels
3636
n_on_line = _burn_lines!(B, geom; fill=!fill)::Int
3737
end
@@ -135,9 +135,9 @@ end
135135

136136
const INTERVALS_INFO = "makes more sense on `Intervals` than `Points` and will have more correct results. You can construct dimensions with a `X(values; sampling=Intervals(Center()))` to achieve this"
137137

138-
@noinline _check_intervals(B) =
139-
_chki(B) ? true : (@info "burning lines $INTERVALS_INFO"; false)
140-
@noinline _check_intervals(B, boundary) =
141-
_chki(B) ? true : (@info "`boundary=:$boundary` $INTERVALS_INFO"; false)
138+
@noinline _check_intervals(B, verbose::Bool) =
139+
_chki(B) ? true : (verbose && @info "burning lines $INTERVALS_INFO"; false)
140+
@noinline _check_intervals(B, boundary, verbose::Bool) =
141+
_chki(B) ? true : (verbose && @info "`boundary=:$boundary` $INTERVALS_INFO"; false)
142142

143143
_chki(B) = all(map(s -> s isa Intervals, sampling(dims(B))))

0 commit comments

Comments
 (0)