Tooling
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
package template
|
||||
|
||||
import "salty/terrain/internal/field"
|
||||
|
||||
// Despeckle removes the hairline of a class nobody painted that appears along a boundary between two others.
|
||||
//
|
||||
// It exists because of a measured failure, and the arithmetic is worth keeping because it will happen to
|
||||
// anybody who exports a lossy image. Classification gives every pixel its nearest class in RGB, and a codec
|
||||
// blends across every boundary it finds. On the first template the antialiased edge between `surf`
|
||||
// (221,238,238) and `lowland` (153,204,102) comes out at about (186,219,174), whose distance to `desert`
|
||||
// (238,221,153) is **53.8** against **77.9** to either of the colours it was actually mixed from. So every
|
||||
// temperate coast on the map gained a one-pixel ribbon of desert - 1607 pixels of it nowhere near the real
|
||||
// desert - and it was invisible until the coast mask made those strays the nearest *land* to a stretch of
|
||||
// open water and handed their class to every cell it turned into shore. An eleven-pixel band of desert
|
||||
// appeared along a green continent, and the mask was blamed for it first.
|
||||
//
|
||||
// **The test is spatial, and it has to be.** The obvious fix is colorimetric - notice that the pixel lies on
|
||||
// the line between two class colours and give it to the nearer one - and it was built, measured and thrown
|
||||
// away, because it cannot work in general and this legend is the proof: `shelf` (153,204,221) sits 10 units
|
||||
// from the line between `ocean` and `surf`, so a real shelf pixel with a little codec noise on it and a
|
||||
// genuine ocean/surf blend are the same point in colour space. That rule reclassified 943 000 painted shelf
|
||||
// pixels. What actually distinguishes a stray is *where* it is: a class nobody painted here occupies a line
|
||||
// one pixel wide with two other classes on either side of it, and no painted feature at 12.9 m a pixel is
|
||||
// one pixel wide - the one thing that was, the decorative stroke, has a pass of its own.
|
||||
//
|
||||
// Hence a five by five window rather than three by three. A one-pixel ribbon running through the middle of a
|
||||
// 3x3 holds three of its nine cells and the two classes it divides hold three each, so nothing has a
|
||||
// majority and the rule cannot fire; over 5x5 the ribbon holds five of twenty-five against ten and ten, which
|
||||
// is the signature being looked for. A feature two pixels wide already holds ten and is left alone.
|
||||
func (r *Raster) Despeckle() int {
|
||||
if r.W < despeckleWindow || r.H < despeckleWindow {
|
||||
return 0
|
||||
}
|
||||
out := make([]uint8, len(r.Class))
|
||||
copy(out, r.Class)
|
||||
|
||||
const rad = despeckleWindow / 2
|
||||
counts := make([]int32, field.BandCount(r.H)*256)
|
||||
changed := make([]int, field.BandCount(r.H))
|
||||
|
||||
field.RowsIndexed(r.H, func(band, y0, y1 int) {
|
||||
c := counts[band*256 : band*256+256]
|
||||
for y := y0; y < y1; y++ {
|
||||
for x := 0; x < r.W; x++ {
|
||||
self := r.Class[y*r.W+x]
|
||||
for dy := -rad; dy <= rad; dy++ {
|
||||
for dx := -rad; dx <= rad; dx++ {
|
||||
c[r.At(x+dx, y+dy)]++
|
||||
}
|
||||
}
|
||||
best, bestN := self, int32(0)
|
||||
for i := range c {
|
||||
if c[i] > bestN || (c[i] == bestN && uint8(i) < best) {
|
||||
best, bestN = uint8(i), c[i]
|
||||
}
|
||||
}
|
||||
selfN := c[self]
|
||||
for i := range c {
|
||||
c[i] = 0
|
||||
}
|
||||
if selfN <= despeckleThin && bestN >= despeckleMajority && best != self {
|
||||
out[y*r.W+x] = best
|
||||
changed[band]++
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
total := 0
|
||||
for _, n := range changed {
|
||||
total += n
|
||||
}
|
||||
r.Class = out
|
||||
return total
|
||||
}
|
||||
|
||||
const (
|
||||
// despeckleWindow is five: see the note above on why three is too small to see a ribbon at all.
|
||||
despeckleWindow = 5
|
||||
|
||||
// despeckleThin is how little of its own window a class may hold and still be called a stray. Five of
|
||||
// twenty-five is a line one pixel wide straight through the middle; a feature two pixels wide holds ten.
|
||||
despeckleThin = 5
|
||||
|
||||
// despeckleMajority is how much of the window the replacement has to hold. Eight of twenty-five means
|
||||
// there is something clearly there to join, so a pixel in genuinely mixed country is left as it is.
|
||||
despeckleMajority = 8
|
||||
)
|
||||
Reference in New Issue
Block a user