Tooling
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
package field
|
||||
|
||||
// SlidingMax is the maximum over a square window, separable and O(1) a cell whatever the radius.
|
||||
//
|
||||
// The naive form is a loop over the window, which is what internal/stats' localRelief used to do and is fine
|
||||
// on the 500 m window it uses at 8 m cells - until the map is a planet. 28 million land cells times a 63-cell
|
||||
// radius is 1.1e11 comparisons, which is not a slow diagnostic, it is one nobody will ever see the end of.
|
||||
// The monotonic deque is the standard answer: each index enters and leaves once, so the row pass is linear in
|
||||
// the row however wide the window.
|
||||
//
|
||||
// wrapX makes the row pass periodic, which is what a cylinder needs; the column pass always clamps, because
|
||||
// the top and bottom of the map are the poles and not each other.
|
||||
func SlidingMax(f *Field, radius int, wrapX bool) *Field {
|
||||
return sliding(f, radius, wrapX, func(inDeque, arriving float32) bool { return inDeque <= arriving })
|
||||
}
|
||||
|
||||
// SlidingMin is the same window, the other way up. The pair is what local relief is made of.
|
||||
func SlidingMin(f *Field, radius int, wrapX bool) *Field {
|
||||
return sliding(f, radius, wrapX, func(inDeque, arriving float32) bool { return inDeque >= arriving })
|
||||
}
|
||||
|
||||
// LocalRelief is max minus min over a square window: the standard field measure of how rugged a place is, and
|
||||
// the one thing slope cannot tell you. A 5 m hummock and a 500 m mountainside both stand at 30 degrees.
|
||||
//
|
||||
// Two sliding passes and a subtract, so it costs the same as one of them twice and nothing per radius. It
|
||||
// holds two fields at once at the peak, which at planet scale is 600 MB - worth saying, because the naive
|
||||
// version held none and could not finish.
|
||||
func LocalRelief(f *Field, radius int, wrapX bool) *Field {
|
||||
hi := SlidingMax(f, radius, wrapX)
|
||||
lo := SlidingMin(f, radius, wrapX)
|
||||
for i := range hi.Data {
|
||||
hi.Data[i] -= lo.Data[i]
|
||||
}
|
||||
return hi
|
||||
}
|
||||
|
||||
// sliding is the shared separable pass. keep reports whether the value already at the back of the deque can
|
||||
// be dropped when a new one arrives, which is the only thing that differs between the maximum and the
|
||||
// minimum: the deque holds indices whose values are monotone, so its front is always the answer for the live
|
||||
// window and anything the arriving value dominates can never be the answer again.
|
||||
func sliding(f *Field, radius int, wrapX bool, keep func(inDeque, arriving float32) bool) *Field {
|
||||
if radius < 1 {
|
||||
return f.Clone()
|
||||
}
|
||||
w, h := f.W, f.H
|
||||
row := New(w, h, f.CellM)
|
||||
buf := make([]float32, 0, w+2*radius)
|
||||
idx := make([]int, 0, w+2*radius)
|
||||
|
||||
for y := 0; y < h; y++ {
|
||||
// The row, extended by the radius at each end so the deque never has to special-case an edge.
|
||||
buf = buf[:0]
|
||||
for x := -radius; x < w+radius; x++ {
|
||||
sx := x
|
||||
if wrapX {
|
||||
sx = ((sx % w) + w) % w
|
||||
} else if sx < 0 {
|
||||
sx = 0
|
||||
} else if sx >= w {
|
||||
sx = w - 1
|
||||
}
|
||||
buf = append(buf, f.Data[y*w+sx])
|
||||
}
|
||||
slide(buf, idx[:0], 2*radius+1, keep, func(i int, v float32) {
|
||||
if i < w {
|
||||
row.Data[y*w+i] = v
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
out := New(w, h, f.CellM)
|
||||
col := make([]float32, 0, h+2*radius)
|
||||
for x := 0; x < w; x++ {
|
||||
col = col[:0]
|
||||
for y := -radius; y < h+radius; y++ {
|
||||
sy := y
|
||||
if sy < 0 {
|
||||
sy = 0
|
||||
} else if sy >= h {
|
||||
sy = h - 1
|
||||
}
|
||||
col = append(col, row.Data[sy*w+x])
|
||||
}
|
||||
slide(col, idx[:0], 2*radius+1, keep, func(i int, v float32) {
|
||||
if i < h {
|
||||
out.Data[i*w+x] = v
|
||||
}
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// slide walks a padded line with a monotonic deque and reports the window's answer ending at each output
|
||||
// position.
|
||||
func slide(line []float32, dq []int, window int, keep func(inDeque, arriving float32) bool,
|
||||
emit func(i int, v float32)) {
|
||||
|
||||
dq = dq[:0]
|
||||
for i, v := range line {
|
||||
for len(dq) > 0 && keep(line[dq[len(dq)-1]], v) {
|
||||
dq = dq[:len(dq)-1]
|
||||
}
|
||||
dq = append(dq, i)
|
||||
if dq[0] <= i-window {
|
||||
dq = dq[1:]
|
||||
}
|
||||
if out := i - window + 1; out >= 0 {
|
||||
emit(out, line[dq[0]])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BoxSmooth blurs a field in place with `passes` of a separable box blur of the given radius, clamping at the
|
||||
// edges. Two passes are near enough to a Gaussian for anything here and cost four linear sweeps.
|
||||
//
|
||||
// Deterministic by construction: fixed traversal order, running sums, no goroutines. It lives here rather than
|
||||
// in the pass that first wanted it because two now do - the coastal detail pass smooths the signed distance to
|
||||
// the shoreline, and the tile bake smooths the interpolated sea floor.
|
||||
func BoxSmooth(data []float32, w, h, radius, passes int) {
|
||||
if radius < 1 || passes < 1 || len(data) < w*h {
|
||||
return
|
||||
}
|
||||
tmp := make([]float32, len(data))
|
||||
for p := 0; p < passes; p++ {
|
||||
boxRows(data, tmp, w, h, radius)
|
||||
boxCols(tmp, data, w, h, radius)
|
||||
}
|
||||
}
|
||||
|
||||
func boxRows(src, dst []float32, w, h, radius int) {
|
||||
n := float32(2*radius + 1)
|
||||
for y := 0; y < h; y++ {
|
||||
row := y * w
|
||||
var sum float32
|
||||
for k := -radius; k <= radius; k++ {
|
||||
sum += src[row+clampIdx(k, w)]
|
||||
}
|
||||
for x := 0; x < w; x++ {
|
||||
dst[row+x] = sum / n
|
||||
sum += src[row+clampIdx(x+radius+1, w)] - src[row+clampIdx(x-radius, w)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func boxCols(src, dst []float32, w, h, radius int) {
|
||||
n := float32(2*radius + 1)
|
||||
for x := 0; x < w; x++ {
|
||||
var sum float32
|
||||
for k := -radius; k <= radius; k++ {
|
||||
sum += src[clampIdx(k, h)*w+x]
|
||||
}
|
||||
for y := 0; y < h; y++ {
|
||||
dst[y*w+x] = sum / n
|
||||
sum += src[clampIdx(y+radius+1, h)*w+x] - src[clampIdx(y-radius, h)*w+x]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func clampIdx(i, n int) int {
|
||||
if i < 0 {
|
||||
return 0
|
||||
}
|
||||
if i >= n {
|
||||
return n - 1
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// BoxSmoothMasked is BoxSmooth restricted to the cells the mask selects: a cell outside it is neither read
|
||||
// nor written, so the blur never averages across the boundary.
|
||||
//
|
||||
// That distinction is the whole reason it exists. The coastal detail pass damps the metre-scale texture near
|
||||
// the shore, and an unmasked blur there does not damp texture, it bridges the waterline: measured on a
|
||||
// fixture with forty metres of water against the land, the plain blur lifted the sea floor by twenty metres.
|
||||
// The step at a shoreline is a landform, not roughness, and a filter that cannot tell them apart is the wrong
|
||||
// filter.
|
||||
//
|
||||
// Separable and weighted: the row pass carries a running sum of values and of weights, the column pass sums
|
||||
// those, and the quotient is the mean over the masked cells in the window. Deterministic, like BoxSmooth.
|
||||
func BoxSmoothMasked(data []float32, mask []bool, w, h, radius, passes int) {
|
||||
if radius < 1 || passes < 1 || len(data) < w*h || len(mask) < w*h {
|
||||
return
|
||||
}
|
||||
n := w * h
|
||||
val := make([]float32, n)
|
||||
wgt := make([]float32, n)
|
||||
tv := make([]float32, n)
|
||||
tw := make([]float32, n)
|
||||
for p := 0; p < passes; p++ {
|
||||
for i := 0; i < n; i++ {
|
||||
if mask[i] {
|
||||
val[i], wgt[i] = data[i], 1
|
||||
} else {
|
||||
val[i], wgt[i] = 0, 0
|
||||
}
|
||||
}
|
||||
boxRowsSum(val, tv, w, h, radius)
|
||||
boxRowsSum(wgt, tw, w, h, radius)
|
||||
boxColsSum(tv, val, w, h, radius)
|
||||
boxColsSum(tw, wgt, w, h, radius)
|
||||
for i := 0; i < n; i++ {
|
||||
if mask[i] && wgt[i] > 0 {
|
||||
data[i] = val[i] / wgt[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// boxRowsSum and boxColsSum are the running sums BoxSmooth uses, without the division: a masked blur needs
|
||||
// the weight sum as well as the value sum, and dividing in the middle would be dividing by the wrong thing.
|
||||
func boxRowsSum(src, dst []float32, w, h, radius int) {
|
||||
for y := 0; y < h; y++ {
|
||||
row := y * w
|
||||
var sum float32
|
||||
for k := -radius; k <= radius; k++ {
|
||||
sum += src[row+clampIdx(k, w)]
|
||||
}
|
||||
for x := 0; x < w; x++ {
|
||||
dst[row+x] = sum
|
||||
sum += src[row+clampIdx(x+radius+1, w)] - src[row+clampIdx(x-radius, w)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func boxColsSum(src, dst []float32, w, h, radius int) {
|
||||
for x := 0; x < w; x++ {
|
||||
var sum float32
|
||||
for k := -radius; k <= radius; k++ {
|
||||
sum += src[clampIdx(k, h)*w+x]
|
||||
}
|
||||
for y := 0; y < h; y++ {
|
||||
dst[y*w+x] = sum
|
||||
sum += src[clampIdx(y+radius+1, h)*w+x] - src[clampIdx(y-radius, h)*w+x]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user