|
1 | 1 | package texturing |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "cmp" |
4 | 5 | "image" |
5 | 6 | "image/color" |
| 7 | + "math" |
6 | 8 |
|
7 | 9 | "github.com/EliCDavis/polyform/drawing/coloring" |
| 10 | + "github.com/EliCDavis/polyform/nodes" |
| 11 | + "github.com/EliCDavis/vector" |
| 12 | + "github.com/EliCDavis/vector/vector1" |
| 13 | + "github.com/EliCDavis/vector/vector2" |
| 14 | + "github.com/EliCDavis/vector/vector3" |
8 | 15 | ) |
9 | 16 |
|
10 | | -func GaussianBlur(src image.Image) image.Image { |
11 | | - dst := image.NewRGBA(src.Bounds()) |
12 | | - ConvolveImage(src, func(x, y int, vals []color.Color) { |
13 | | - x1y1 := coloring.MultiplyRGBByConstant(vals[0], 1./16) |
14 | | - x2y1 := coloring.MultiplyRGBByConstant(vals[1], 2./16) |
15 | | - x3y1 := coloring.MultiplyRGBByConstant(vals[2], 1./16) |
16 | | - |
17 | | - x1y2 := coloring.MultiplyRGBByConstant(vals[3], 2./16) |
18 | | - x2y2 := coloring.MultiplyRGBByConstant(vals[4], 4./16) |
19 | | - x3y2 := coloring.MultiplyRGBByConstant(vals[5], 2./16) |
20 | | - |
21 | | - x1y3 := coloring.MultiplyRGBByConstant(vals[6], 1./16) |
22 | | - x2y3 := coloring.MultiplyRGBByConstant(vals[7], 2./16) |
23 | | - x3y3 := coloring.MultiplyRGBByConstant(vals[8], 1./16) |
24 | | - |
25 | | - dst.Set(x, y, coloring.AddRGB( |
26 | | - x1y1, x2y1, x3y1, |
27 | | - x1y2, x2y2, x3y2, |
28 | | - x1y3, x2y3, x3y3, |
29 | | - )) |
30 | | - }) |
31 | | - return dst |
32 | | -} |
33 | | - |
34 | 17 | func boxBlur(src image.Image, dst *image.RGBA) { |
35 | 18 | ConvolveImage(src, func(x, y int, vals []color.Color) { |
36 | | - x1y1 := coloring.MultiplyRGBByConstant(vals[0], 1./9) |
37 | | - x2y1 := coloring.MultiplyRGBByConstant(vals[1], 1./9) |
38 | | - x3y1 := coloring.MultiplyRGBByConstant(vals[2], 1./9) |
| 19 | + x1y1 := coloring.ScaleRGB(vals[0], 1./9) |
| 20 | + x2y1 := coloring.ScaleRGB(vals[1], 1./9) |
| 21 | + x3y1 := coloring.ScaleRGB(vals[2], 1./9) |
39 | 22 |
|
40 | | - x1y2 := coloring.MultiplyRGBByConstant(vals[3], 1./9) |
41 | | - x2y2 := coloring.MultiplyRGBByConstant(vals[4], 1./9) |
42 | | - x3y2 := coloring.MultiplyRGBByConstant(vals[5], 1./9) |
| 23 | + x1y2 := coloring.ScaleRGB(vals[3], 1./9) |
| 24 | + x2y2 := coloring.ScaleRGB(vals[4], 1./9) |
| 25 | + x3y2 := coloring.ScaleRGB(vals[5], 1./9) |
43 | 26 |
|
44 | | - x1y3 := coloring.MultiplyRGBByConstant(vals[6], 1./9) |
45 | | - x2y3 := coloring.MultiplyRGBByConstant(vals[7], 1./9) |
46 | | - x3y3 := coloring.MultiplyRGBByConstant(vals[8], 1./9) |
| 27 | + x1y3 := coloring.ScaleRGB(vals[6], 1./9) |
| 28 | + x2y3 := coloring.ScaleRGB(vals[7], 1./9) |
| 29 | + x3y3 := coloring.ScaleRGB(vals[8], 1./9) |
47 | 30 |
|
48 | 31 | dst.Set(x, y, coloring.AddRGB( |
49 | 32 | x1y1, x2y1, x3y1, |
@@ -82,3 +65,137 @@ func BoxBlurNTimes(src image.Image, iterations int) image.Image { |
82 | 65 | } |
83 | 66 | return dst |
84 | 67 | } |
| 68 | + |
| 69 | +// Generates a normalized 1D Gaussian kernel |
| 70 | +func gaussianKernel(radius int, sigma float64) []float64 { |
| 71 | + kernel := make([]float64, 2*radius+1) |
| 72 | + var sum float64 |
| 73 | + |
| 74 | + sigma2 := sigma * sigma * 2 |
| 75 | + for i := -radius; i <= radius; i++ { |
| 76 | + v := math.Exp(-(float64(i * i)) / sigma2) |
| 77 | + kernel[i+radius] = v |
| 78 | + sum += v |
| 79 | + } |
| 80 | + for i := range kernel { |
| 81 | + kernel[i] /= sum |
| 82 | + } |
| 83 | + return kernel |
| 84 | +} |
| 85 | + |
| 86 | +func clamp[T cmp.Ordered](i, minimum, maximum T) T { |
| 87 | + return max(min(i, maximum), minimum) |
| 88 | +} |
| 89 | + |
| 90 | +// Applies a 1D convolution along x or y |
| 91 | +func convolve1DGaussian[T any](space vector.Space[T], src Texture[T], dst Texture[T], kernel []float64, horizontal bool) { |
| 92 | + radius := len(kernel) / 2 |
| 93 | + |
| 94 | + if horizontal { |
| 95 | + src.ScanParallel(func(x, y int, v T) { |
| 96 | + var accum T |
| 97 | + for k := -radius; k <= radius; k++ { |
| 98 | + sx := clamp(x+k, 0, src.width-1) |
| 99 | + weighted := space.Scale(src.Get(sx, y), kernel[k+radius]) |
| 100 | + accum = space.Add(accum, weighted) |
| 101 | + } |
| 102 | + dst.Set(x, y, accum) |
| 103 | + }) |
| 104 | + } else { |
| 105 | + src.ScanParallel(func(x, y int, v T) { |
| 106 | + var accum T |
| 107 | + for k := -radius; k <= radius; k++ { |
| 108 | + sy := clamp(y+k, 0, src.height-1) |
| 109 | + weighted := space.Scale(src.Get(x, sy), kernel[k+radius]) |
| 110 | + accum = space.Add(accum, weighted) |
| 111 | + } |
| 112 | + dst.Set(x, y, accum) |
| 113 | + }) |
| 114 | + } |
| 115 | +} |
| 116 | + |
| 117 | +// GaussianBlur applies a Gaussian blur to the texture |
| 118 | +func RadialGaussianBlur[T any](space vector.Space[T], src Texture[T], radius int, sigma float64) Texture[T] { |
| 119 | + kernel := gaussianKernel(radius, sigma) |
| 120 | + |
| 121 | + tmp := NewTexture[T](src.width, src.height) |
| 122 | + out := NewTexture[T](src.width, src.height) |
| 123 | + |
| 124 | + // Horizontal then vertical |
| 125 | + convolve1DGaussian(space, src, tmp, kernel, true) |
| 126 | + convolve1DGaussian(space, tmp, out, kernel, false) |
| 127 | + |
| 128 | + return out |
| 129 | +} |
| 130 | + |
| 131 | +type GaussianBlurFloatNode struct { |
| 132 | + Texture nodes.Output[Texture[float64]] `description:"Texture to blur"` |
| 133 | + Radius nodes.Output[int] `description:"Size of the kernel in pixels"` |
| 134 | + Signma nodes.Output[float64] `description:"standard deviation of the gaussian"` |
| 135 | +} |
| 136 | + |
| 137 | +func (n GaussianBlurFloatNode) Value(out *nodes.StructOutput[Texture[float64]]) { |
| 138 | + if n.Texture == nil { |
| 139 | + return |
| 140 | + } |
| 141 | + out.Set(RadialGaussianBlur( |
| 142 | + vector1.Space[float64]{}, |
| 143 | + nodes.GetOutputValue(out, n.Texture), |
| 144 | + nodes.TryGetOutputValue(out, n.Radius, 1), |
| 145 | + nodes.TryGetOutputValue(out, n.Signma, 1), |
| 146 | + )) |
| 147 | +} |
| 148 | + |
| 149 | +type GaussianBlurFloat2Node struct { |
| 150 | + Texture nodes.Output[Texture[vector2.Float64]] `description:"Texture to blur"` |
| 151 | + Radius nodes.Output[int] `description:"Size of the kernel in pixels"` |
| 152 | + Signma nodes.Output[float64] `description:"standard deviation of the gaussian"` |
| 153 | +} |
| 154 | + |
| 155 | +func (n GaussianBlurFloat2Node) Value(out *nodes.StructOutput[Texture[vector2.Float64]]) { |
| 156 | + if n.Texture == nil { |
| 157 | + return |
| 158 | + } |
| 159 | + out.Set(RadialGaussianBlur( |
| 160 | + vector2.Space[float64]{}, |
| 161 | + nodes.GetOutputValue(out, n.Texture), |
| 162 | + nodes.TryGetOutputValue(out, n.Radius, 1), |
| 163 | + nodes.TryGetOutputValue(out, n.Signma, 1), |
| 164 | + )) |
| 165 | +} |
| 166 | + |
| 167 | +type GaussianBlurFloat3Node struct { |
| 168 | + Texture nodes.Output[Texture[vector3.Float64]] `description:"Texture to blur"` |
| 169 | + Radius nodes.Output[int] `description:"Size of the kernel in pixels"` |
| 170 | + Signma nodes.Output[float64] `description:"standard deviation of the gaussian"` |
| 171 | +} |
| 172 | + |
| 173 | +func (n GaussianBlurFloat3Node) Value(out *nodes.StructOutput[Texture[vector3.Float64]]) { |
| 174 | + if n.Texture == nil { |
| 175 | + return |
| 176 | + } |
| 177 | + out.Set(RadialGaussianBlur( |
| 178 | + vector3.Space[float64]{}, |
| 179 | + nodes.GetOutputValue(out, n.Texture), |
| 180 | + nodes.TryGetOutputValue(out, n.Radius, 1), |
| 181 | + nodes.TryGetOutputValue(out, n.Signma, 1), |
| 182 | + )) |
| 183 | +} |
| 184 | + |
| 185 | +type GaussianBlurColorNode struct { |
| 186 | + Texture nodes.Output[Texture[coloring.Color]] `description:"Texture to blur"` |
| 187 | + Radius nodes.Output[int] `description:"Size of the kernel in pixels"` |
| 188 | + Signma nodes.Output[float64] `description:"standard deviation of the gaussian"` |
| 189 | +} |
| 190 | + |
| 191 | +func (n GaussianBlurColorNode) Value(out *nodes.StructOutput[Texture[coloring.Color]]) { |
| 192 | + if n.Texture == nil { |
| 193 | + return |
| 194 | + } |
| 195 | + out.Set(RadialGaussianBlur( |
| 196 | + coloring.Space{}, |
| 197 | + nodes.GetOutputValue(out, n.Texture), |
| 198 | + nodes.TryGetOutputValue(out, n.Radius, 1), |
| 199 | + nodes.TryGetOutputValue(out, n.Signma, 1), |
| 200 | + )) |
| 201 | +} |
0 commit comments