github.com/aloncn/graphics-go@v0.0.1/graphics/convolve/convolve.go (about) 1 // Copyright 2011 The Graphics-Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package convolve 6 7 import ( 8 "errors" 9 "fmt" 10 "image" 11 "image/draw" 12 "math" 13 ) 14 15 // clamp clamps x to the range [x0, x1]. 16 func clamp(x, x0, x1 float64) float64 { 17 if x < x0 { 18 return x0 19 } 20 if x > x1 { 21 return x1 22 } 23 return x 24 } 25 26 // Kernel is a square matrix that defines a convolution. 27 type Kernel interface { 28 // Weights returns the square matrix of weights in row major order. 29 Weights() []float64 30 } 31 32 // SeparableKernel is a linearly separable, square convolution kernel. 33 // X and Y are the per-axis weights. Each slice must be the same length, and 34 // have an odd length. The middle element of each slice is the weight for the 35 // central pixel. For example, the horizontal Sobel kernel is: 36 // sobelX := &SeparableKernel{ 37 // X: []float64{-1, 0, +1}, 38 // Y: []float64{1, 2, 1}, 39 // } 40 type SeparableKernel struct { 41 X, Y []float64 42 } 43 44 func (k *SeparableKernel) Weights() []float64 { 45 n := len(k.X) 46 w := make([]float64, n*n) 47 for y := 0; y < n; y++ { 48 for x := 0; x < n; x++ { 49 w[y*n+x] = k.X[x] * k.Y[y] 50 } 51 } 52 return w 53 } 54 55 // fullKernel is a square convolution kernel. 56 type fullKernel []float64 57 58 func (k fullKernel) Weights() []float64 { return k } 59 60 func kernelSize(w []float64) (size int, err error) { 61 size = int(math.Sqrt(float64(len(w)))) 62 if size*size != len(w) { 63 return 0, errors.New("graphics: kernel is not square") 64 } 65 if size%2 != 1 { 66 return 0, errors.New("graphics: kernel size is not odd") 67 } 68 return size, nil 69 } 70 71 // NewKernel returns a square convolution kernel. 72 func NewKernel(w []float64) (Kernel, error) { 73 if _, err := kernelSize(w); err != nil { 74 return nil, err 75 } 76 return fullKernel(w), nil 77 } 78 79 func convolveRGBASep(dst *image.RGBA, src image.Image, k *SeparableKernel) error { 80 if len(k.X) != len(k.Y) { 81 return fmt.Errorf("graphics: kernel not square (x %d, y %d)", len(k.X), len(k.Y)) 82 } 83 if len(k.X)%2 != 1 { 84 return fmt.Errorf("graphics: kernel length (%d) not odd", len(k.X)) 85 } 86 radius := (len(k.X) - 1) / 2 87 88 // buf holds the result of vertically blurring src. 89 bounds := dst.Bounds() 90 width, height := bounds.Dx(), bounds.Dy() 91 buf := make([]float64, width*height*4) 92 for y := bounds.Min.Y; y < bounds.Max.Y; y++ { 93 for x := bounds.Min.X; x < bounds.Max.X; x++ { 94 var r, g, b, a float64 95 // k0 is the kernel weight for the center pixel. This may be greater 96 // than kernel[0], near the boundary of the source image, to avoid 97 // vignetting. 98 k0 := k.X[radius] 99 100 // Add the pixels from above. 101 for i := 1; i <= radius; i++ { 102 f := k.Y[radius-i] 103 if y-i < bounds.Min.Y { 104 k0 += f 105 } else { 106 or, og, ob, oa := src.At(x, y-i).RGBA() 107 r += float64(or>>8) * f 108 g += float64(og>>8) * f 109 b += float64(ob>>8) * f 110 a += float64(oa>>8) * f 111 } 112 } 113 114 // Add the pixels from below. 115 for i := 1; i <= radius; i++ { 116 f := k.Y[radius+i] 117 if y+i >= bounds.Max.Y { 118 k0 += f 119 } else { 120 or, og, ob, oa := src.At(x, y+i).RGBA() 121 r += float64(or>>8) * f 122 g += float64(og>>8) * f 123 b += float64(ob>>8) * f 124 a += float64(oa>>8) * f 125 } 126 } 127 128 // Add the central pixel. 129 or, og, ob, oa := src.At(x, y).RGBA() 130 r += float64(or>>8) * k0 131 g += float64(og>>8) * k0 132 b += float64(ob>>8) * k0 133 a += float64(oa>>8) * k0 134 135 // Write to buf. 136 o := (y-bounds.Min.Y)*width*4 + (x-bounds.Min.X)*4 137 buf[o+0] = r 138 buf[o+1] = g 139 buf[o+2] = b 140 buf[o+3] = a 141 } 142 } 143 144 // dst holds the result of horizontally blurring buf. 145 for y := 0; y < height; y++ { 146 for x := 0; x < width; x++ { 147 var r, g, b, a float64 148 k0, off := k.X[radius], y*width*4+x*4 149 150 // Add the pixels from the left. 151 for i := 1; i <= radius; i++ { 152 f := k.X[radius-i] 153 if x-i < 0 { 154 k0 += f 155 } else { 156 o := off - i*4 157 r += buf[o+0] * f 158 g += buf[o+1] * f 159 b += buf[o+2] * f 160 a += buf[o+3] * f 161 } 162 } 163 164 // Add the pixels from the right. 165 for i := 1; i <= radius; i++ { 166 f := k.X[radius+i] 167 if x+i >= width { 168 k0 += f 169 } else { 170 o := off + i*4 171 r += buf[o+0] * f 172 g += buf[o+1] * f 173 b += buf[o+2] * f 174 a += buf[o+3] * f 175 } 176 } 177 178 // Add the central pixel. 179 r += buf[off+0] * k0 180 g += buf[off+1] * k0 181 b += buf[off+2] * k0 182 a += buf[off+3] * k0 183 184 // Write to dst, clamping to the range [0, 255]. 185 dstOff := (y-dst.Rect.Min.Y)*dst.Stride + (x-dst.Rect.Min.X)*4 186 dst.Pix[dstOff+0] = uint8(clamp(r+0.5, 0, 255)) 187 dst.Pix[dstOff+1] = uint8(clamp(g+0.5, 0, 255)) 188 dst.Pix[dstOff+2] = uint8(clamp(b+0.5, 0, 255)) 189 dst.Pix[dstOff+3] = uint8(clamp(a+0.5, 0, 255)) 190 } 191 } 192 193 return nil 194 } 195 196 func convolveRGBA(dst *image.RGBA, src image.Image, k Kernel) error { 197 b := dst.Bounds() 198 bs := src.Bounds() 199 w := k.Weights() 200 size, err := kernelSize(w) 201 if err != nil { 202 return err 203 } 204 radius := (size - 1) / 2 205 206 for y := b.Min.Y; y < b.Max.Y; y++ { 207 for x := b.Min.X; x < b.Max.X; x++ { 208 if !image.Pt(x, y).In(bs) { 209 continue 210 } 211 212 var r, g, b, a, adj float64 213 for cy := y - radius; cy <= y+radius; cy++ { 214 for cx := x - radius; cx <= x+radius; cx++ { 215 factor := w[(cy-y+radius)*size+cx-x+radius] 216 if !image.Pt(cx, cy).In(bs) { 217 adj += factor 218 } else { 219 sr, sg, sb, sa := src.At(cx, cy).RGBA() 220 r += float64(sr>>8) * factor 221 g += float64(sg>>8) * factor 222 b += float64(sb>>8) * factor 223 a += float64(sa>>8) * factor 224 } 225 } 226 } 227 228 if adj != 0 { 229 sr, sg, sb, sa := src.At(x, y).RGBA() 230 r += float64(sr>>8) * adj 231 g += float64(sg>>8) * adj 232 b += float64(sb>>8) * adj 233 a += float64(sa>>8) * adj 234 } 235 236 off := (y-dst.Rect.Min.Y)*dst.Stride + (x-dst.Rect.Min.X)*4 237 dst.Pix[off+0] = uint8(clamp(r+0.5, 0, 0xff)) 238 dst.Pix[off+1] = uint8(clamp(g+0.5, 0, 0xff)) 239 dst.Pix[off+2] = uint8(clamp(b+0.5, 0, 0xff)) 240 dst.Pix[off+3] = uint8(clamp(a+0.5, 0, 0xff)) 241 } 242 } 243 244 return nil 245 } 246 247 // Convolve produces dst by applying the convolution kernel k to src. 248 func Convolve(dst draw.Image, src image.Image, k Kernel) (err error) { 249 if dst == nil || src == nil || k == nil { 250 return nil 251 } 252 253 b := dst.Bounds() 254 dstRgba, ok := dst.(*image.RGBA) 255 if !ok { 256 dstRgba = image.NewRGBA(b) 257 } 258 259 switch k := k.(type) { 260 case *SeparableKernel: 261 err = convolveRGBASep(dstRgba, src, k) 262 default: 263 err = convolveRGBA(dstRgba, src, k) 264 } 265 266 if err != nil { 267 return err 268 } 269 270 if !ok { 271 draw.Draw(dst, b, dstRgba, b.Min, draw.Src) 272 } 273 return nil 274 }